Compare commits
568 Commits
fd4dfc2dff
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
9
.gitignore
vendored
9
.gitignore
vendored
@@ -44,6 +44,7 @@ captures/
|
||||
|
||||
# IntelliJ
|
||||
*.iml
|
||||
.idea/deviceManager.xml
|
||||
.idea/workspace.xml
|
||||
.idea/tasks.xml
|
||||
.idea/gradle.xml
|
||||
@@ -57,6 +58,9 @@ 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
|
||||
|
||||
# Keystore files
|
||||
# Uncomment the following lines if you do not want to check your keystore files in.
|
||||
#*.jks
|
||||
@@ -140,6 +144,7 @@ output.json
|
||||
hs_err_pid*
|
||||
|
||||
### Kotlin ###
|
||||
.kotlin/
|
||||
# Compiled class file
|
||||
|
||||
# Log file
|
||||
@@ -306,4 +311,8 @@ fabric.properties
|
||||
app/debug/
|
||||
app/release/
|
||||
|
||||
docs/
|
||||
.junie/
|
||||
.kiro/
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/macos,android,androidstudio,visualstudiocode,git,kotlin,java
|
||||
|
||||
35
.idea/codeStyles/Project.xml
generated
35
.idea/codeStyles/Project.xml
generated
@@ -1,5 +1,40 @@
|
||||
<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>
|
||||
|
||||
4
.idea/deploymentTargetSelector.xml
generated
4
.idea/deploymentTargetSelector.xml
generated
@@ -4,10 +4,10 @@
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2024-09-30T12:57:26.128603Z">
|
||||
<DropdownSelection timestamp="2025-10-23T14:41:22.468459Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="LocalEmulator" identifier="path=/Users/klaus/.android/avd/Pixel_8_Pro_API_34.avd" />
|
||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=ce0917195d15ab39017e" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
|
||||
318
.idea/other.xml
generated
318
.idea/other.xml
generated
@@ -1,318 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="direct_access_persist.xml">
|
||||
<option name="deviceSelectionList">
|
||||
<list>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="27" />
|
||||
<option name="brand" value="DOCOMO" />
|
||||
<option name="codename" value="F01L" />
|
||||
<option name="id" value="F01L" />
|
||||
<option name="manufacturer" value="FUJITSU" />
|
||||
<option name="name" value="F-01L" />
|
||||
<option name="screenDensity" value="360" />
|
||||
<option name="screenX" value="720" />
|
||||
<option name="screenY" value="1280" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="28" />
|
||||
<option name="brand" value="DOCOMO" />
|
||||
<option name="codename" value="SH-01L" />
|
||||
<option name="id" value="SH-01L" />
|
||||
<option name="manufacturer" value="SHARP" />
|
||||
<option name="name" value="AQUOS sense2 SH-01L" />
|
||||
<option name="screenDensity" value="480" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2160" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="31" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="a51" />
|
||||
<option name="id" value="a51" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy A51" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="akita" />
|
||||
<option name="id" value="akita" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 8a" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="b0q" />
|
||||
<option name="id" value="b0q" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy S22 Ultra" />
|
||||
<option name="screenDensity" value="600" />
|
||||
<option name="screenX" value="1440" />
|
||||
<option name="screenY" value="3088" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="32" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="bluejay" />
|
||||
<option name="id" value="bluejay" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 6a" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="caiman" />
|
||||
<option name="id" value="caiman" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 9 Pro" />
|
||||
<option name="screenDensity" value="360" />
|
||||
<option name="screenX" value="960" />
|
||||
<option name="screenY" value="2142" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="comet" />
|
||||
<option name="id" value="comet" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 9 Pro Fold" />
|
||||
<option name="screenDensity" value="390" />
|
||||
<option name="screenX" value="2076" />
|
||||
<option name="screenY" value="2152" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="29" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="crownqlteue" />
|
||||
<option name="id" value="crownqlteue" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy Note9" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="2220" />
|
||||
<option name="screenY" value="1080" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="dm3q" />
|
||||
<option name="id" value="dm3q" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy S23 Ultra" />
|
||||
<option name="screenDensity" value="600" />
|
||||
<option name="screenX" value="1440" />
|
||||
<option name="screenY" value="3088" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="e1q" />
|
||||
<option name="id" value="e1q" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy S24" />
|
||||
<option name="screenDensity" value="480" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2340" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="felix" />
|
||||
<option name="id" value="felix" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel Fold" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="2208" />
|
||||
<option name="screenY" value="1840" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="felix" />
|
||||
<option name="id" value="felix" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel Fold" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="2208" />
|
||||
<option name="screenY" value="1840" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="felix_camera" />
|
||||
<option name="id" value="felix_camera" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel Fold (Camera-enabled)" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="2208" />
|
||||
<option name="screenY" value="1840" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="gts8uwifi" />
|
||||
<option name="id" value="gts8uwifi" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy Tab S8 Ultra" />
|
||||
<option name="screenDensity" value="320" />
|
||||
<option name="screenX" value="1848" />
|
||||
<option name="screenY" value="2960" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="husky" />
|
||||
<option name="id" value="husky" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 8 Pro" />
|
||||
<option name="screenDensity" value="390" />
|
||||
<option name="screenX" value="1008" />
|
||||
<option name="screenY" value="2244" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="30" />
|
||||
<option name="brand" value="motorola" />
|
||||
<option name="codename" value="java" />
|
||||
<option name="id" value="java" />
|
||||
<option name="manufacturer" value="Motorola" />
|
||||
<option name="name" value="G20" />
|
||||
<option name="screenDensity" value="280" />
|
||||
<option name="screenX" value="720" />
|
||||
<option name="screenY" value="1600" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="komodo" />
|
||||
<option name="id" value="komodo" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 9 Pro XL" />
|
||||
<option name="screenDensity" value="360" />
|
||||
<option name="screenX" value="1008" />
|
||||
<option name="screenY" value="2244" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="lynx" />
|
||||
<option name="id" value="lynx" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 7a" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="31" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="oriole" />
|
||||
<option name="id" value="oriole" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 6" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="panther" />
|
||||
<option name="id" value="panther" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 7" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="q5q" />
|
||||
<option name="id" value="q5q" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy Z Fold5" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1812" />
|
||||
<option name="screenY" value="2176" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="30" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="r11" />
|
||||
<option name="id" value="r11" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel Watch" />
|
||||
<option name="screenDensity" value="320" />
|
||||
<option name="screenX" value="384" />
|
||||
<option name="screenY" value="384" />
|
||||
<option name="type" value="WEAR_OS" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="30" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="redfin" />
|
||||
<option name="id" value="redfin" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 5" />
|
||||
<option name="screenDensity" value="440" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2340" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="shiba" />
|
||||
<option name="id" value="shiba" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 8" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="tangorpro" />
|
||||
<option name="id" value="tangorpro" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel Tablet" />
|
||||
<option name="screenDensity" value="320" />
|
||||
<option name="screenX" value="1600" />
|
||||
<option name="screenY" value="2560" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="tokay" />
|
||||
<option name="id" value="tokay" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 9" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2424" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="29" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="x1q" />
|
||||
<option name="id" value="x1q" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy S20" />
|
||||
<option name="screenDensity" value="480" />
|
||||
<option name="screenX" value="1440" />
|
||||
<option name="screenY" value="3200" />
|
||||
</PersistentDeviceSelectionData>
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
187
app/build.gradle
187
app/build.gradle
@@ -1,27 +1,28 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
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'
|
||||
}
|
||||
|
||||
android {
|
||||
namespace 'kr.co.vividnext.sodalive'
|
||||
compileSdk 34
|
||||
compileSdk = 35
|
||||
|
||||
viewBinding {
|
||||
enabled true
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
dataBinding true
|
||||
buildConfig true
|
||||
}
|
||||
|
||||
dependenciesInfo {
|
||||
@@ -31,12 +32,39 @@ 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 34
|
||||
versionCode 109
|
||||
versionName "1.19.2"
|
||||
targetSdk 35
|
||||
versionCode 204
|
||||
versionName "1.44.0"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@@ -49,10 +77,23 @@ android {
|
||||
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"'
|
||||
manifestPlaceholders = [
|
||||
URISCHEME : "voiceon",
|
||||
APPLINK_HOST : "voiceon.onelink.me",
|
||||
FACEBOOK_APP_ID : "612448298237287",
|
||||
FACEBOOK_CLIENT_TOKEN: "32af760f4a7b7cb7e3b1e7ffd0b0da70",
|
||||
KAKAO_APP_KEY : "231cf78acfa8252fca38b9eedf87c5cb"
|
||||
]
|
||||
}
|
||||
|
||||
debug {
|
||||
minifyEnabled true
|
||||
minifyEnabled false
|
||||
debuggable true
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
applicationIdSuffix '.debug'
|
||||
@@ -62,15 +103,25 @@ android {
|
||||
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"'
|
||||
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
|
||||
@@ -78,16 +129,17 @@ android {
|
||||
}
|
||||
|
||||
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") {
|
||||
@@ -107,56 +159,105 @@ dependencies {
|
||||
}
|
||||
|
||||
// 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.4.3"
|
||||
|
||||
// agora
|
||||
implementation "io.agora.rtc:voice-sdk:4.2.6"
|
||||
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.michalsvec:single-row-calednar:1.0.0"
|
||||
implementation 'com.github.bumptech.glide:glide:5.0.5'
|
||||
annotationProcessor 'com.github.bumptech.glide:compiler:5.0.5'
|
||||
|
||||
// google in-app-purchase
|
||||
implementation "com.android.billingclient:billing-ktx:6.2.0"
|
||||
implementation "com.android.billingclient:billing-ktx:8.0.0"
|
||||
|
||||
// ROOM
|
||||
kapt "androidx.room:room-compiler:2.5.0"
|
||||
implementation "androidx.room:room-ktx:2.5.0"
|
||||
implementation "androidx.room:room-runtime:2.5.0"
|
||||
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'
|
||||
|
||||
// 노티플라이
|
||||
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'
|
||||
|
||||
// ----- 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'
|
||||
}
|
||||
|
||||
|
||||
// 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
|
||||
}
|
||||
14
app/proguard-rules.pro
vendored
14
app/proguard-rules.pro
vendored
@@ -229,3 +229,17 @@
|
||||
|
||||
# @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** { *; }
|
||||
|
||||
1
app/schemas/.gitkeep
Normal file
1
app/schemas/.gitkeep
Normal file
@@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,13 @@
|
||||
<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" />
|
||||
@@ -12,11 +19,8 @@
|
||||
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"
|
||||
@@ -34,7 +38,6 @@
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
@@ -46,19 +49,12 @@
|
||||
<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" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
<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"
|
||||
@@ -68,6 +64,39 @@
|
||||
android:theme="@style/Theme.SodaLive"
|
||||
android:usesCleartextTraffic="true"
|
||||
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">
|
||||
@@ -76,28 +105,23 @@
|
||||
|
||||
<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=".user.login.LoginActivity" />
|
||||
<activity android:name=".user.signup.SignUpActivity" />
|
||||
<activity android:name=".audio_content.all.AudioContentAllActivity" />
|
||||
<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.point.PointStatusActivity" />
|
||||
<activity
|
||||
android:name=".mypage.can.charge.CanChargeActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden" />
|
||||
<activity android:name=".mypage.can.payment.CanPaymentActivity" />
|
||||
<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" />
|
||||
@@ -123,6 +147,7 @@
|
||||
<activity android:name=".settings.event.EventActivity" />
|
||||
<activity android:name=".settings.event.EventDetailActivity" />
|
||||
<activity android:name=".settings.notification.NotificationSettingsActivity" />
|
||||
<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" />
|
||||
@@ -134,18 +159,28 @@
|
||||
<activity android:name=".live.now.all.LiveNowAllActivity" />
|
||||
<activity android:name=".live.reservation.all.LiveReservationAllActivity" />
|
||||
<activity android:name=".mypage.service_center.ServiceCenterActivity" />
|
||||
<activity android:name=".message.MessageActivity" />
|
||||
<activity android:name=".onboarding.OnBoardingActivity" />
|
||||
<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.curation.AudioContentCurationActivity" />
|
||||
<activity android:name=".audio_content.all.AudioContentNewAllActivity" />
|
||||
<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" />
|
||||
@@ -161,6 +196,9 @@
|
||||
<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"
|
||||
@@ -169,6 +207,22 @@
|
||||
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"
|
||||
@@ -179,6 +233,15 @@
|
||||
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
|
||||
android:name=".fcm.SodaFirebaseMessagingService"
|
||||
@@ -209,5 +272,46 @@
|
||||
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>
|
||||
|
||||
25
app/src/main/assets/payverse_starter.html
Normal file
25
app/src/main/assets/payverse_starter.html
Normal file
@@ -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>
|
||||
25
app/src/main/assets/payverse_starter_debug.html
Normal file
25
app/src/main/assets/payverse_starter_debug.html
Normal file
@@ -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 {
|
||||
initRtcEngine()
|
||||
initRtmClient()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun deInitAgoraEngine(rtmEventListener: RtmEventListener) {
|
||||
deInitRtcEngine()
|
||||
deInitRtmClient(rtmEventListener)
|
||||
}
|
||||
|
||||
// region RtcEngine
|
||||
private var rtcEngine: RtcEngine? = null
|
||||
|
||||
@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)
|
||||
|
||||
rtmClient = RtmClient.createInstance(
|
||||
context,
|
||||
BuildConfig.AGORA_APP_ID,
|
||||
rtmClientListener
|
||||
)
|
||||
} 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 inputChat(message: String) {
|
||||
val rtmMessage = rtmClient!!.createMessage()
|
||||
rtmMessage.text = message
|
||||
|
||||
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}")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
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,37 +101,304 @@ class Agora(
|
||||
rtcEngine?.muteAllRemoteAudioStreams(mute)
|
||||
}
|
||||
|
||||
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
|
||||
) {
|
||||
val option = SendMessageOptions()
|
||||
|
||||
val message = rtmClient!!.createMessage()
|
||||
message.rawMessage = rawMessage ?: requestType.toString().toByteArray()
|
||||
|
||||
rtmClient!!.sendMessageToPeer(
|
||||
receiverUid,
|
||||
if (roomChannelName != null) {
|
||||
val message = rawMessage ?: requestType.toString().toByteArray()
|
||||
val options = PublishOptions()
|
||||
options.customType = "ByteArray"
|
||||
rtmClient!!.publish(
|
||||
"inbox_$receiverUid",
|
||||
message,
|
||||
option,
|
||||
object : ResultCallback<Void?> {
|
||||
override fun onSuccess(aVoid: Void?) {
|
||||
options,
|
||||
object : ResultCallback<Void> {
|
||||
override fun onSuccess(p0: Void?) {
|
||||
Logger.e("sendMessage - onSuccess")
|
||||
onSuccess()
|
||||
}
|
||||
|
||||
override fun onFailure(errorInfo: ErrorInfo) {
|
||||
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 rtmChannelIsNull(): Boolean {
|
||||
return rtmChannel == 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
|
||||
}
|
||||
|
||||
fun getConnectionState(): Int {
|
||||
return rtcEngine!!.connectionState
|
||||
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
|
||||
}
|
||||
|
||||
@@ -5,16 +5,26 @@ 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 kr.co.vividnext.sodalive.BuildConfig
|
||||
import kr.co.vividnext.sodalive.common.ImageLoaderProvider
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
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 {
|
||||
@@ -29,6 +39,14 @@ class SodaLiveApp : Application() {
|
||||
SharedPreferenceManager.init(applicationContext)
|
||||
|
||||
ImageLoaderProvider.init(applicationContext)
|
||||
|
||||
FacebookSdk.fullyInitialize()
|
||||
|
||||
KakaoSdk.init(applicationContext, BuildConfig.KAKAO_APP_KEY)
|
||||
|
||||
setupAppsFlyer()
|
||||
|
||||
setupNotifly()
|
||||
}
|
||||
|
||||
private fun isDebuggable(): Boolean {
|
||||
@@ -44,10 +62,81 @@ 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("딥링크를 찾을 수 없습니다.")
|
||||
DeepLinkResult.Status.ERROR -> Logger.d("딥링크 처리 중 오류 발생: ${deepLinkResult.error}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun logUtmInFirebase() {
|
||||
FirebaseTracking.logUtm()
|
||||
}
|
||||
|
||||
private fun setupNotifly() {
|
||||
Notifly.initialize(
|
||||
applicationContext,
|
||||
BuildConfig.NOTIFLY_PROJECT_ID,
|
||||
BuildConfig.NOTIFLY_USERNAME,
|
||||
BuildConfig.NOTIFLY_PASSWORD,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onStart(owner: LifecycleOwner) {
|
||||
super.onStart(owner)
|
||||
isAppInForeground = true
|
||||
}
|
||||
|
||||
override fun onStop(owner: LifecycleOwner) {
|
||||
isAppInForeground = false
|
||||
}
|
||||
|
||||
companion object {
|
||||
var isAppInForeground = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,12 @@ class AudioContentAdapter(
|
||||
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)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
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
|
||||
import kr.co.vividnext.sodalive.audio_content.curation.GetCurationContentResponse
|
||||
import kr.co.vividnext.sodalive.audio_content.detail.GetAudioContentDetailResponse
|
||||
import kr.co.vividnext.sodalive.audio_content.detail.PutAudioContentLikeRequest
|
||||
import kr.co.vividnext.sodalive.audio_content.detail.PutAudioContentLikeResponse
|
||||
@@ -15,12 +15,14 @@ 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.GetAudioContentRanking
|
||||
import kr.co.vividnext.sodalive.audio_content.main.GetNewContentUploadCreator
|
||||
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 kr.co.vividnext.sodalive.settings.ContentType
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.RequestBody
|
||||
import retrofit2.http.Body
|
||||
@@ -35,24 +37,56 @@ import retrofit2.http.Path
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface AudioContentApi {
|
||||
@GET("/audio-content/all")
|
||||
fun getAllAudioContents(
|
||||
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
|
||||
@Query("contentType") contentType: ContentType,
|
||||
@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("isAdultContentVisible") isAdultContentVisible: Boolean,
|
||||
@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(
|
||||
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
|
||||
@Query("contentType") contentType: ContentType,
|
||||
@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("isAdultContentVisible") isAdultContentVisible: Boolean,
|
||||
@Query("contentType") contentType: ContentType,
|
||||
@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("isAdultContentVisible") isAdultContentVisible: Boolean,
|
||||
@Query("contentType") contentType: ContentType,
|
||||
@Query("page") page: Int,
|
||||
@Query("size") size: Int,
|
||||
@Query("sort-type") sort: AudioContentViewModel.Sort,
|
||||
@@ -141,12 +175,17 @@ interface AudioContentApi {
|
||||
@GET("/audio-content/main/new")
|
||||
fun getNewContentOfTheme(
|
||||
@Query("theme") theme: String,
|
||||
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
|
||||
@Query("contentType") contentType: ContentType,
|
||||
@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("isAdultContentVisible") isAdultContentVisible: Boolean,
|
||||
@Query("contentType") contentType: ContentType,
|
||||
@Query("page") page: Int,
|
||||
@Query("size") size: Int,
|
||||
@Header("Authorization") authHeader: String
|
||||
@@ -164,17 +203,10 @@ interface AudioContentApi {
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<Any>>
|
||||
|
||||
@GET("/audio-content/curation/{id}")
|
||||
fun getAudioContentListByCurationId(
|
||||
@Path("id") id: Long,
|
||||
@Query("page") page: Int,
|
||||
@Query("size") size: Int,
|
||||
@Query("sort-type") sort: AudioContentViewModel.Sort,
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<GetCurationContentResponse>>
|
||||
|
||||
@GET("/audio-content/main/theme")
|
||||
fun getNewContentThemeList(
|
||||
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
|
||||
@Query("contentType") contentType: ContentType,
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<List<String>>>
|
||||
|
||||
@@ -193,16 +225,13 @@ interface AudioContentApi {
|
||||
|
||||
@GET("/audio-content/main/curation-list")
|
||||
fun getCurationList(
|
||||
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
|
||||
@Query("contentType") contentType: ContentType,
|
||||
@Query("page") page: Int,
|
||||
@Query("size") size: Int,
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<List<GetAudioContentCurationResponse>>>
|
||||
|
||||
@GET("/audio-content/main/new-content-upload-creator")
|
||||
fun getNewContentUploadCreatorList(
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<List<GetNewContentUploadCreator>>>
|
||||
|
||||
@GET("/audio-content/main/banner-list")
|
||||
fun getMainBannerList(
|
||||
@Header("Authorization") authHeader: String
|
||||
@@ -224,4 +253,10 @@ interface AudioContentApi {
|
||||
@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>>
|
||||
}
|
||||
|
||||
@@ -203,6 +203,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) {
|
||||
@@ -314,6 +322,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) {
|
||||
@@ -481,7 +506,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 ?: "")
|
||||
@@ -578,6 +603,6 @@ class AudioContentPlayService :
|
||||
}
|
||||
|
||||
enum class MusicAction {
|
||||
PLAY, PAUSE, STOP, PROGRESS, INIT, CONDITIONAL_STOP
|
||||
PLAY, PAUSE, STOP, PROGRESS, INIT, CONDITIONAL_STOP, SEEK_FORWARD, SEEK_BACKWARD
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,31 +5,16 @@ 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.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.settings.ContentType
|
||||
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 getAudioContentListByCurationId(
|
||||
curationId: Long,
|
||||
page: Int,
|
||||
size: Int,
|
||||
sort: AudioContentViewModel.Sort = AudioContentViewModel.Sort.NEWEST,
|
||||
token: String
|
||||
) = api.getAudioContentListByCurationId(
|
||||
id = curationId,
|
||||
page = page - 1,
|
||||
size = size,
|
||||
sort = sort,
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
fun getAudioContentList(
|
||||
id: Long,
|
||||
categoryId: Long,
|
||||
@@ -40,12 +25,19 @@ class AudioContentRepository(
|
||||
) = api.getAudioContentList(
|
||||
id = id,
|
||||
categoryId = categoryId,
|
||||
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
|
||||
page = page - 1,
|
||||
size = size,
|
||||
sort = sort,
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
fun getAudioContentReplayLiveList(token: String) = api.getAudioContentReplayLiveList(
|
||||
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
|
||||
contentType = ContentType.entries[SharedPreferenceManager.contentPreference],
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
fun getAudioContentThemeList(token: String) = api.getAudioContentThemeList(token)
|
||||
|
||||
fun uploadAudioContent(
|
||||
@@ -84,22 +76,6 @@ class AudioContentRepository(
|
||||
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,
|
||||
@@ -133,24 +109,27 @@ class AudioContentRepository(
|
||||
token: String
|
||||
) = api.likeContent(request, authHeader = token)
|
||||
|
||||
fun getNewContentOfTheme(theme: String, token: String) = api.getNewContentOfTheme(
|
||||
theme = theme,
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
fun getNewContentAllOfTheme(
|
||||
isFree: Boolean,
|
||||
theme: String,
|
||||
page: Int,
|
||||
size: Int,
|
||||
token: String
|
||||
) = api.getNewContentAllOfTheme(
|
||||
isFree = isFree,
|
||||
theme = theme,
|
||||
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
|
||||
contentType = ContentType.entries[SharedPreferenceManager.contentPreference],
|
||||
page = page - 1,
|
||||
size = size,
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
fun getNewContentThemeList(token: String) = api.getNewContentThemeList(authHeader = token)
|
||||
fun getNewContentThemeList(token: String) = api.getNewContentThemeList(
|
||||
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
|
||||
contentType = ContentType.entries[SharedPreferenceManager.contentPreference],
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
fun donation(
|
||||
contentId: Long,
|
||||
@@ -180,18 +159,6 @@ class AudioContentRepository(
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
fun getCurationList(page: Int, size: Int, token: String) = api.getCurationList(
|
||||
page = page - 1,
|
||||
size = size,
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
fun getNewContentUploadCreatorList(
|
||||
token: String
|
||||
) = api.getNewContentUploadCreatorList(authHeader = token)
|
||||
|
||||
fun getMainBannerList(token: String) = api.getMainBannerList(authHeader = token)
|
||||
fun getMainOrderList(token: String) = api.getMainOrderList(authHeader = token)
|
||||
fun pinContent(
|
||||
audioContentId: Long,
|
||||
token: String
|
||||
@@ -215,9 +182,43 @@ class AudioContentRepository(
|
||||
token: String
|
||||
) = api.getAudioContentByTheme(
|
||||
id = themeId,
|
||||
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
|
||||
contentType = ContentType.entries[SharedPreferenceManager.contentPreference],
|
||||
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(
|
||||
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
|
||||
contentType = ContentType.entries[SharedPreferenceManager.contentPreference],
|
||||
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(
|
||||
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
|
||||
contentType = ContentType.entries[SharedPreferenceManager.contentPreference],
|
||||
isFree = isFree,
|
||||
isPointAvailableOnly = isPointAvailableOnly,
|
||||
authHeader = token
|
||||
)
|
||||
}
|
||||
|
||||
@@ -41,7 +41,10 @@ class AudioContentViewModel(private val repository: AudioContentRepository) : Ba
|
||||
PRICE_HIGH,
|
||||
|
||||
@SerializedName("PRICE_LOW")
|
||||
PRICE_LOW
|
||||
PRICE_LOW,
|
||||
|
||||
@SerializedName("POPULARITY")
|
||||
POPULARITY
|
||||
}
|
||||
|
||||
var isLast = false
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
package kr.co.vividnext.sodalive.audio_content
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import io.objectbox.annotation.Entity
|
||||
import io.objectbox.annotation.Id
|
||||
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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
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
|
||||
|
||||
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 -> "포인트 대여 전체"
|
||||
isFree -> "무료 콘텐츠 전체"
|
||||
else -> "콘텐츠 전체보기"
|
||||
}
|
||||
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 {
|
||||
adapter.addItems(emptyList())
|
||||
viewModel.selectTheme(it, 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) {
|
||||
themeAdapter.addItems(it)
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
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.AudioContentViewModel
|
||||
import kr.co.vividnext.sodalive.base.BaseViewModel
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
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
|
||||
) {
|
||||
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) {
|
||||
val themeList = listOf("전체").union(it.data).toList()
|
||||
_themeListLiveData.postValue(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 loadAll(
|
||||
isFree: Boolean? = null,
|
||||
isPointAvailableOnly: Boolean? = null
|
||||
) {
|
||||
if (_isLoading.value == true || isLast) return
|
||||
_isLoading.value = true
|
||||
|
||||
compositeDisposable.add(
|
||||
repository.getAllAudioContents(
|
||||
page = page,
|
||||
size = size,
|
||||
isFree = isFree,
|
||||
isPointAvailableOnly = isPointAvailableOnly,
|
||||
sortType = _sortLiveData.value!!,
|
||||
theme = if (selectedTheme == "전체") {
|
||||
null
|
||||
} else {
|
||||
selectedTheme
|
||||
},
|
||||
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 ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,17 @@
|
||||
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.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.GridSpacingItemDecoration
|
||||
@@ -17,8 +19,10 @@ import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentNewAllBinding
|
||||
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import kr.co.vividnext.sodalive.home.HomeContentThemeAdapter
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
class AudioContentNewAllActivity : BaseActivity<ActivityAudioContentNewAllBinding>(
|
||||
ActivityAudioContentNewAllBinding::inflate
|
||||
) {
|
||||
@@ -26,30 +30,45 @@ class AudioContentNewAllActivity : BaseActivity<ActivityAudioContentNewAllBindin
|
||||
|
||||
private lateinit var loadingDialog: LoadingDialog
|
||||
|
||||
private lateinit var newContentThemeAdapter: AudioContentMainNewContentThemeAdapter
|
||||
private lateinit var newContentThemeAdapter: HomeContentThemeAdapter
|
||||
private lateinit var newContentAdapter: AudioContentNewAllAdapter
|
||||
|
||||
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()
|
||||
viewModel.getNewContentList(isFree = isFree)
|
||||
}
|
||||
|
||||
override fun setupView() {
|
||||
loadingDialog = LoadingDialog(this, layoutInflater)
|
||||
binding.toolbar.tvBack.text = "새로운 콘텐츠"
|
||||
binding.toolbar.tvBack.text = if (isFree) {
|
||||
"새로운 무료 콘텐츠"
|
||||
} else {
|
||||
"새로운 단편"
|
||||
}
|
||||
binding.toolbar.tvBack.setOnClickListener { finish() }
|
||||
|
||||
binding.tvNotice.text = if (isFree) {
|
||||
"※ 최근 2주간 등록된 새로운 콘텐츠 입니다."
|
||||
} else {
|
||||
"※ 최근 2주간 등록된 새로운 단편 입니다."
|
||||
}
|
||||
|
||||
setupNewContentTheme()
|
||||
setupNewContent()
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
private fun setupNewContentTheme() {
|
||||
newContentThemeAdapter = AudioContentMainNewContentThemeAdapter {
|
||||
newContentThemeAdapter = HomeContentThemeAdapter {
|
||||
newContentAdapter.clear()
|
||||
viewModel.selectTheme(it)
|
||||
newContentAdapter.notifyDataSetChanged()
|
||||
viewModel.selectTheme(it, isFree = isFree)
|
||||
}
|
||||
|
||||
binding.rvNewContentTheme.layoutManager = LinearLayoutManager(
|
||||
@@ -90,10 +109,11 @@ class AudioContentNewAllActivity : BaseActivity<ActivityAudioContentNewAllBindin
|
||||
}
|
||||
|
||||
private fun setupNewContent() {
|
||||
val spanCount = 3
|
||||
val spacing = 40
|
||||
// 아이템 정사각형 크기 계산: (screenWidth - (16*2) - 16) / 2
|
||||
// 아이템 정사각형 크기 계산: (screenWidth - (paddingHorizontal*2) - itemSpacing) / 2
|
||||
val itemSize = ((screenWidth - 16f.dpToPx() * 2 - 16f.dpToPx()) / 2f).toInt()
|
||||
newContentAdapter = AudioContentNewAllAdapter(
|
||||
itemWidth = (screenWidth - (spacing * (spanCount + 1))) / 3,
|
||||
itemWidth = itemSize,
|
||||
onClickItem = {
|
||||
startActivity(
|
||||
Intent(this, AudioContentDetailActivity::class.java).apply {
|
||||
@@ -110,8 +130,12 @@ class AudioContentNewAllActivity : BaseActivity<ActivityAudioContentNewAllBindin
|
||||
}
|
||||
)
|
||||
|
||||
val spanCount = 2
|
||||
val spacingPx = 16f.dpToPx().toInt()
|
||||
binding.rvContent.layoutManager = GridLayoutManager(this, spanCount)
|
||||
binding.rvContent.addItemDecoration(GridSpacingItemDecoration(spanCount, spacing, true))
|
||||
binding.rvContent.addItemDecoration(
|
||||
GridSpacingItemDecoration(spanCount, spacingPx, true)
|
||||
)
|
||||
|
||||
binding.rvContent.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
@@ -125,7 +149,7 @@ class AudioContentNewAllActivity : BaseActivity<ActivityAudioContentNewAllBindin
|
||||
if (!recyclerView.canScrollVertically(1) &&
|
||||
lastVisibleItemPosition == itemTotalCount
|
||||
) {
|
||||
viewModel.getNewContentList()
|
||||
viewModel.getNewContentList(isFree)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -151,6 +175,13 @@ class AudioContentNewAllActivity : BaseActivity<ActivityAudioContentNewAllBindin
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@ package kr.co.vividnext.sodalive.audio_content.all
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -15,12 +13,9 @@ 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 com.bumptech.glide.request.target.CustomTarget
|
||||
import com.bumptech.glide.request.transition.Transition
|
||||
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.dpToPx
|
||||
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
||||
|
||||
class AudioContentNewAllAdapter(
|
||||
@@ -47,11 +42,18 @@ class AudioContentNewAllAdapter(
|
||||
)
|
||||
.into(binding.ivAudioContentCoverImage)
|
||||
|
||||
val layoutParams = binding.ivAudioContentCoverImage.layoutParams as ConstraintLayout.LayoutParams
|
||||
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)
|
||||
@@ -94,7 +96,7 @@ class AudioContentNewAllAdapter(
|
||||
|
||||
override fun getItemCount() = items.size
|
||||
|
||||
override fun onBindViewHolder(holder: AudioContentNewAllAdapter.ViewHolder, position: Int) {
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.bind(items[position])
|
||||
}
|
||||
|
||||
|
||||
@@ -38,12 +38,13 @@ class AudioContentNewAllViewModel(
|
||||
private val size = 10
|
||||
private var selectedTheme = ""
|
||||
|
||||
fun getNewContentList() {
|
||||
fun getNewContentList(isFree: Boolean = false) {
|
||||
if (!_isLoading.value!! && !isLast) {
|
||||
_isLoading.value = true
|
||||
|
||||
compositeDisposable.add(
|
||||
repository.getNewContentAllOfTheme(
|
||||
isFree = isFree,
|
||||
theme = if (selectedTheme == "전체") {
|
||||
""
|
||||
} else {
|
||||
@@ -60,11 +61,12 @@ class AudioContentNewAllViewModel(
|
||||
if (it.success && it.data != null) {
|
||||
if (it.data.items.isNotEmpty()) {
|
||||
page += 1
|
||||
_newContentListLiveData.postValue(it.data.items)
|
||||
_newContentTotalCountLiveData.postValue(it.data.totalCount)
|
||||
} else {
|
||||
isLast = true
|
||||
}
|
||||
|
||||
_newContentListLiveData.postValue(it.data.items)
|
||||
_newContentTotalCountLiveData.postValue(it.data.totalCount)
|
||||
} else {
|
||||
if (it.message != null) {
|
||||
_toastLiveData.postValue(it.message)
|
||||
@@ -118,10 +120,10 @@ class AudioContentNewAllViewModel(
|
||||
)
|
||||
}
|
||||
|
||||
fun selectTheme(theme: String) {
|
||||
fun selectTheme(theme: String, isFree: Boolean) {
|
||||
isLast = false
|
||||
page = 1
|
||||
selectedTheme = theme
|
||||
getNewContentList()
|
||||
getNewContentList(isFree)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ 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
|
||||
@@ -29,6 +30,12 @@ class AudioContentRankingAllAdapter(
|
||||
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
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.all.by_theme
|
||||
|
||||
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.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
|
||||
@@ -20,7 +20,6 @@ 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.explorer.profile.UserProfileActivity
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
class AudioContentAllByThemeActivity : BaseActivity<ActivityAudioContentAllByThemeBinding>(
|
||||
@@ -52,6 +51,7 @@ class AudioContentAllByThemeActivity : BaseActivity<ActivityAudioContentAllByThe
|
||||
viewModel.getContentList(themeId = themeId)
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun setupView() {
|
||||
loadingDialog = LoadingDialog(this, layoutInflater)
|
||||
binding.toolbar.tvBack.setOnClickListener { finish() }
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.box
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
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 = "내 보관함"
|
||||
binding.toolbar.tvBack.setOnClickListener { finish() }
|
||||
}
|
||||
|
||||
private fun setupTabs() {
|
||||
val tabs = binding.tabs
|
||||
tabs.addTab(tabs.newTab().setText("구매목록"))
|
||||
tabs.addTab(tabs.newTab().setText("재생목록"))
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.load
|
||||
import coil.transform.CircleCropTransformation
|
||||
import com.orhanobut.logger.Logger
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.databinding.ItemAudioContentCommentBinding
|
||||
@@ -32,6 +31,8 @@ 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 {
|
||||
@@ -113,12 +114,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)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -155,10 +160,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
|
||||
@@ -176,7 +181,7 @@ class AudioContentCommentAdapter(
|
||||
}
|
||||
|
||||
R.id.menu_review_delete -> {
|
||||
onClickDelete(commentId)
|
||||
onClickDelete()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.curation
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
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.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.ActivityAudioContentCurationBinding
|
||||
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
class AudioContentCurationActivity : BaseActivity<ActivityAudioContentCurationBinding>(
|
||||
ActivityAudioContentCurationBinding::inflate
|
||||
) {
|
||||
|
||||
private val viewModel: AudioContentCurationViewModel by inject()
|
||||
|
||||
private lateinit var loadingDialog: LoadingDialog
|
||||
private lateinit var adapter: AudioContentNewAllAdapter
|
||||
|
||||
private var curationId: Long = 0
|
||||
private lateinit var title: String
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
title = intent.getStringExtra(Constants.EXTRA_AUDIO_CONTENT_CURATION_TITLE) ?: ""
|
||||
curationId = intent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_CURATION_ID, 0)
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (title.isBlank() || curationId <= 0) {
|
||||
Toast.makeText(applicationContext, "잘못된 요청입니다.", Toast.LENGTH_LONG).show()
|
||||
finish()
|
||||
}
|
||||
|
||||
bindData()
|
||||
viewModel.getContentList(curationId = curationId)
|
||||
}
|
||||
|
||||
override fun setupView() {
|
||||
loadingDialog = LoadingDialog(this, layoutInflater)
|
||||
binding.toolbar.tvBack.text = title
|
||||
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(
|
||||
Intent(this, UserProfileActivity::class.java).apply {
|
||||
putExtra(Constants.EXTRA_USER_ID, it)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
binding.rvCuration.layoutManager = GridLayoutManager(this, spanCount)
|
||||
binding.rvCuration.addItemDecoration(GridSpacingItemDecoration(spanCount, spacing, true))
|
||||
|
||||
binding.rvCuration.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(curationId)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
binding.rvCuration.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)
|
||||
}
|
||||
}
|
||||
|
||||
@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.contentListLiveData.observe(this) {
|
||||
if (viewModel.page - 1 == 1) {
|
||||
adapter.clear()
|
||||
binding.rvCuration.scrollToPosition(0)
|
||||
}
|
||||
|
||||
binding.tvTotalCount.text = "${it.totalCount}"
|
||||
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(curationId = curationId)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.curation
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
|
||||
|
||||
@Keep
|
||||
data class GetCurationContentResponse(
|
||||
@SerializedName("totalCount") val totalCount: Int,
|
||||
@SerializedName("items") val items: List<GetAudioContentMainItem>
|
||||
)
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.detail
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.Service
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@@ -21,20 +19,24 @@ import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.load
|
||||
import coil.transform.CircleCropTransformation
|
||||
import coil.transform.RoundedCornersTransformation
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.google.gson.Gson
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.audio_content.AudioContentPlayService
|
||||
import kr.co.vividnext.sodalive.audio_content.PurchaseOption
|
||||
import kr.co.vividnext.sodalive.audio_content.comment.AudioContentCommentFragment
|
||||
import kr.co.vividnext.sodalive.audio_content.modify.AudioContentModifyActivity
|
||||
import kr.co.vividnext.sodalive.audio_content.order.AudioContentOrderConfirmDialog
|
||||
import kr.co.vividnext.sodalive.audio_content.order.AudioContentOrderFragment
|
||||
import kr.co.vividnext.sodalive.audio_content.order.OrderType
|
||||
import kr.co.vividnext.sodalive.audio_content.player.AudioContentPlayerService
|
||||
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||
import kr.co.vividnext.sodalive.base.SodaDialog
|
||||
import kr.co.vividnext.sodalive.common.Constants
|
||||
@@ -52,14 +54,18 @@ import kr.co.vividnext.sodalive.mypage.auth.AuthVerifyRequest
|
||||
import kr.co.vividnext.sodalive.mypage.auth.BootpayResponse
|
||||
import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeActivity
|
||||
import kr.co.vividnext.sodalive.mypage.can.payment.CanPaymentTempActivity
|
||||
import kr.co.vividnext.sodalive.mypage.recent.RecentContentViewModel
|
||||
import kr.co.vividnext.sodalive.mypage.recent.db.RecentContent
|
||||
import kr.co.vividnext.sodalive.report.ReportType
|
||||
import org.koin.android.ext.android.inject
|
||||
import kotlin.math.ceil
|
||||
|
||||
@UnstableApi
|
||||
class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBinding>(
|
||||
ActivityAudioContentDetailBinding::inflate
|
||||
) {
|
||||
private val viewModel: AudioContentDetailViewModel by inject()
|
||||
private val recentContentViewModel: RecentContentViewModel by inject()
|
||||
|
||||
private lateinit var loadingDialog: LoadingDialog
|
||||
private lateinit var creatorOtherContentAdapter: OtherContentAdapter
|
||||
@@ -76,6 +82,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
||||
private var title = ""
|
||||
|
||||
private lateinit var activityResultLauncher: ActivityResultLauncher<Intent>
|
||||
private lateinit var contentBuyerAdapter: AudioContentBuyerAdapter
|
||||
private lateinit var audioContent: GetAudioContentDetailResponse
|
||||
private lateinit var orderType: OrderType
|
||||
private lateinit var imm: InputMethodManager
|
||||
@@ -99,7 +106,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
||||
audioContentId = intent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0)
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
imm = getSystemService(Service.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
|
||||
if (audioContentId <= 0) {
|
||||
Toast.makeText(applicationContext, "잘못된 요청입니다.", Toast.LENGTH_LONG).show()
|
||||
@@ -109,7 +116,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
||||
activityResultLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) {
|
||||
if (it.resultCode == Activity.RESULT_OK) {
|
||||
if (it.resultCode == RESULT_OK) {
|
||||
contentOrder(audioContent, orderType)
|
||||
}
|
||||
}
|
||||
@@ -118,16 +125,18 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
||||
viewModel.getAudioContentDetail(audioContentId = audioContentId) { finish() }
|
||||
}
|
||||
|
||||
@SuppressLint("UnspecifiedRegisterReceiverFlag")
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
val intentFilter = IntentFilter(Constants.ACTION_AUDIO_CONTENT_RECEIVER)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
registerReceiver(audioContentReceiver, intentFilter, Context.RECEIVER_NOT_EXPORTED)
|
||||
registerReceiver(audioContentReceiver, intentFilter, RECEIVER_NOT_EXPORTED)
|
||||
} else {
|
||||
registerReceiver(audioContentReceiver, intentFilter)
|
||||
}
|
||||
|
||||
if (refresh) {
|
||||
contentBuyerAdapter.clear()
|
||||
viewModel.getAudioContentDetail(audioContentId = audioContentId) { finish() }
|
||||
}
|
||||
}
|
||||
@@ -163,6 +172,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
||||
)
|
||||
|
||||
binding.swipeRefreshLayout.setOnRefreshListener {
|
||||
contentBuyerAdapter.clear()
|
||||
viewModel.getAudioContentDetail(
|
||||
audioContentId = audioContentId
|
||||
) { finish() }
|
||||
@@ -292,6 +302,36 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
||||
|
||||
dialog.show(screenWidth - 26.7f.dpToPx().toInt())
|
||||
}
|
||||
|
||||
setupBuyerList()
|
||||
}
|
||||
|
||||
private fun setupBuyerList() {
|
||||
val recyclerView = binding.rvBuyer
|
||||
contentBuyerAdapter = AudioContentBuyerAdapter()
|
||||
|
||||
recyclerView.layoutManager = LinearLayoutManager(
|
||||
this,
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
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.top = 6.7f.dpToPx().toInt()
|
||||
outRect.bottom = 6.7f.dpToPx().toInt()
|
||||
outRect.left = 6.7f.dpToPx().toInt()
|
||||
outRect.right = 6.7f.dpToPx().toInt()
|
||||
}
|
||||
})
|
||||
recyclerView.adapter = contentBuyerAdapter
|
||||
}
|
||||
|
||||
private fun donation(can: Int, message: String) {
|
||||
@@ -312,7 +352,8 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
||||
if (viewModel.audioContentLiveData.value!!.isAvailablePin) {
|
||||
viewModel.pinContent(audioContentId)
|
||||
} else {
|
||||
SodaDialog(this@AudioContentDetailActivity,
|
||||
SodaDialog(
|
||||
this@AudioContentDetailActivity,
|
||||
layoutInflater,
|
||||
"고정 한도 도달",
|
||||
"이 콘텐츠를 고정하시겠어요? " +
|
||||
@@ -412,6 +453,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
||||
setupInfoArea(it)
|
||||
setupPurchaseButton(it)
|
||||
setupCommentArea(it)
|
||||
setupPreviousNextContentArea(it.previousContent, it.nextContent)
|
||||
setupCreatorOtherContentListArea(it.creatorOtherContentList)
|
||||
setupSameThemeOtherContentList(it.sameThemeOtherContentList)
|
||||
}
|
||||
@@ -459,6 +501,78 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupPreviousNextContentArea(
|
||||
previousContent: OtherContentResponse?,
|
||||
nextContent: OtherContentResponse?
|
||||
) {
|
||||
binding.llPreviousNextContent.visibility = if (
|
||||
previousContent != null ||
|
||||
nextContent != null
|
||||
) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
|
||||
if (previousContent != null) {
|
||||
binding.llPreviousContent.visibility = View.VISIBLE
|
||||
binding.viewPreviousNone.visibility = View.GONE
|
||||
|
||||
binding.ivPreviousCover.load(previousContent.coverUrl) {
|
||||
crossfade(true)
|
||||
placeholder(R.drawable.bg_placeholder)
|
||||
transformations(RoundedCornersTransformation(5.3f.dpToPx()))
|
||||
}
|
||||
|
||||
binding.tvPreviousTitle.text = previousContent.title
|
||||
|
||||
binding.llPreviousContent.setOnClickListener {
|
||||
startActivity(
|
||||
Intent(
|
||||
applicationContext,
|
||||
AudioContentDetailActivity::class.java
|
||||
).apply {
|
||||
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, previousContent.contentId)
|
||||
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
binding.viewPreviousNone.visibility = View.VISIBLE
|
||||
binding.llPreviousContent.visibility = View.GONE
|
||||
}
|
||||
|
||||
if (nextContent != null) {
|
||||
binding.llNextContent.visibility = View.VISIBLE
|
||||
binding.viewNextNone.visibility = View.GONE
|
||||
|
||||
binding.ivNextCover.load(nextContent.coverUrl) {
|
||||
crossfade(true)
|
||||
placeholder(R.drawable.bg_placeholder)
|
||||
transformations(RoundedCornersTransformation(5.3f.dpToPx()))
|
||||
}
|
||||
|
||||
binding.tvNextTitle.text = nextContent.title
|
||||
|
||||
binding.llNextContent.setOnClickListener {
|
||||
startActivity(
|
||||
Intent(
|
||||
applicationContext,
|
||||
AudioContentDetailActivity::class.java
|
||||
).apply {
|
||||
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, nextContent.contentId)
|
||||
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
binding.viewNextNone.visibility = View.VISIBLE
|
||||
binding.llNextContent.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupCommentArea(response: GetAudioContentDetailResponse) {
|
||||
if (
|
||||
response.isCommentAvailable &&
|
||||
@@ -585,27 +699,39 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
||||
"캔으로"
|
||||
}
|
||||
|
||||
binding.tvStrPurchaseOrRental.text = if (response.isOnlyRental) {
|
||||
" 대여하기"
|
||||
} else {
|
||||
" 구매하기"
|
||||
when (response.purchaseOption) {
|
||||
PurchaseOption.BOTH -> {
|
||||
binding.tvStrPurchaseOrRental.text = " 구매하기"
|
||||
binding.llPurchase.setBackgroundResource(
|
||||
R.drawable.bg_round_corner_5_3_3bb9f1
|
||||
)
|
||||
}
|
||||
|
||||
binding.llPurchase.setOnClickListener {
|
||||
if (
|
||||
response.totalContentCount != null &&
|
||||
PurchaseOption.BUY_ONLY -> {
|
||||
binding.tvStrPurchaseOrRental.text = " 소장하기"
|
||||
binding.llPurchase.setBackgroundResource(
|
||||
R.drawable.bg_round_corner_5_3_59548f
|
||||
)
|
||||
}
|
||||
|
||||
PurchaseOption.RENT_ONLY -> {
|
||||
binding.tvStrPurchaseOrRental.text = " 대여하기"
|
||||
binding.llPurchase.setBackgroundResource(
|
||||
R.drawable.bg_round_corner_5_3_548f7d
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val limitedEdition = response.totalContentCount != null &&
|
||||
response.remainingContentCount != null
|
||||
) {
|
||||
showOrderConfirmDialog(
|
||||
audioContent = response,
|
||||
isOnlyRental = false,
|
||||
OrderType.KEEP
|
||||
)
|
||||
|
||||
binding.llPurchase.setOnClickListener {
|
||||
if (limitedEdition || response.purchaseOption == PurchaseOption.BUY_ONLY) {
|
||||
showOrderConfirmDialog(audioContent = response, OrderType.KEEP)
|
||||
} else if (response.purchaseOption == PurchaseOption.RENT_ONLY) {
|
||||
showOrderConfirmDialog(audioContent = response, OrderType.RENTAL)
|
||||
} else {
|
||||
showOrderDialog(
|
||||
audioContent = response,
|
||||
isOnlyRental = response.isOnlyRental
|
||||
)
|
||||
showOrderDialog(audioContent = response)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -628,6 +754,8 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
||||
binding.flSoldOut.visibility = View.GONE
|
||||
binding.tvSoldOutBig.visibility = View.GONE
|
||||
binding.ivPlayOrPause.visibility = View.GONE
|
||||
binding.ivSeekBackward10.visibility = View.GONE
|
||||
binding.ivSeekForward10.visibility = View.GONE
|
||||
binding.tvPreviewNo.visibility = View.GONE
|
||||
binding.tvTotalDuration.text = " / ${response.duration}"
|
||||
|
||||
@@ -636,7 +764,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
||||
response.price > 0
|
||||
|
||||
if (
|
||||
!response.existOrdered &&
|
||||
response.creator.creatorId != SharedPreferenceManager.userId && !response.existOrdered &&
|
||||
response.totalContentCount != null && response.remainingContentCount != null &&
|
||||
response.remainingContentCount <= 0
|
||||
) {
|
||||
@@ -650,7 +778,18 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
||||
binding.ivPlayOrPause.visibility = View.VISIBLE
|
||||
binding.ivPlayOrPause.setOnClickListener {
|
||||
startService(
|
||||
Intent(this, AudioContentPlayService::class.java).apply {
|
||||
Intent(
|
||||
applicationContext,
|
||||
AudioContentPlayerService::class.java
|
||||
).apply {
|
||||
action = "STOP_SERVICE"
|
||||
}
|
||||
)
|
||||
startService(
|
||||
Intent(
|
||||
applicationContext,
|
||||
AudioContentPlayService::class.java
|
||||
).apply {
|
||||
putExtra(
|
||||
Constants.EXTRA_AUDIO_CONTENT_COVER_IMAGE_URL,
|
||||
response.coverImageUrl
|
||||
@@ -670,6 +809,15 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
recentContentViewModel.insertRecentContent(
|
||||
RecentContent(
|
||||
contentId = response.contentId,
|
||||
coverImageUrl = response.coverImageUrl,
|
||||
title = response.title,
|
||||
creatorNickname = response.creator.nickname
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
binding.ivPlayOrPause.setImageResource(
|
||||
@@ -679,9 +827,42 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
||||
R.drawable.btn_audio_content_preview_play
|
||||
}
|
||||
)
|
||||
|
||||
if (!isAlertPreview) {
|
||||
binding.ivSeekForward10.visibility = View.VISIBLE
|
||||
binding.ivSeekBackward10.visibility = View.VISIBLE
|
||||
|
||||
binding.ivSeekForward10.setOnClickListener {
|
||||
startService(
|
||||
Intent(
|
||||
applicationContext,
|
||||
AudioContentPlayService::class.java
|
||||
).apply {
|
||||
action = AudioContentPlayService.MusicAction.SEEK_FORWARD.name
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
binding.ivSeekBackward10.setOnClickListener {
|
||||
startService(
|
||||
Intent(
|
||||
applicationContext,
|
||||
AudioContentPlayService::class.java
|
||||
).apply {
|
||||
action = AudioContentPlayService.MusicAction.SEEK_BACKWARD.name
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (response.releaseDate == null) {
|
||||
binding.tvPreviewNo.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
binding.ivPoint.visibility = if (response.isAvailableUsePoint) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
@@ -755,11 +936,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
||||
|
||||
binding.tvShare.visibility = View.VISIBLE
|
||||
binding.tvShare.setOnClickListener {
|
||||
viewModel.shareAudioContent(
|
||||
audioContentId = audioContentId,
|
||||
contentImage = response.coverImageUrl,
|
||||
contentTitle = "${response.title} - ${response.creator.nickname}"
|
||||
) {
|
||||
viewModel.shareContent(audioContentId = audioContentId) {
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
intent.type = "text/plain"
|
||||
intent.putExtra(Intent.EXTRA_TEXT, it)
|
||||
@@ -797,8 +974,16 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
||||
|
||||
binding.tvRemainingCount.text = "${response.remainingContentCount}"
|
||||
}
|
||||
|
||||
if (response.buyerList.isNotEmpty()) {
|
||||
binding.rvBuyer.visibility = View.VISIBLE
|
||||
binding.tvBuyerTitle.visibility = View.VISIBLE
|
||||
contentBuyerAdapter.addItems(response.buyerList)
|
||||
}
|
||||
} else {
|
||||
binding.rlLimitedEdition.visibility = View.GONE
|
||||
binding.rvBuyer.visibility = View.GONE
|
||||
binding.tvBuyerTitle.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -895,15 +1080,11 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
||||
}
|
||||
}
|
||||
|
||||
private fun showOrderDialog(
|
||||
audioContent: GetAudioContentDetailResponse,
|
||||
isOnlyRental: Boolean = false
|
||||
) {
|
||||
private fun showOrderDialog(audioContent: GetAudioContentDetailResponse) {
|
||||
val dialog = AudioContentOrderFragment(
|
||||
price = audioContent.price,
|
||||
isOnlyRental = isOnlyRental,
|
||||
onClickKeep = { showOrderConfirmDialog(audioContent, isOnlyRental, OrderType.KEEP) },
|
||||
onClickRental = { showOrderConfirmDialog(audioContent, isOnlyRental, OrderType.RENTAL) }
|
||||
onClickKeep = { showOrderConfirmDialog(audioContent, OrderType.KEEP) },
|
||||
onClickRental = { showOrderConfirmDialog(audioContent, OrderType.RENTAL) }
|
||||
)
|
||||
|
||||
dialog.show(
|
||||
@@ -914,7 +1095,6 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
||||
|
||||
private fun showOrderConfirmDialog(
|
||||
audioContent: GetAudioContentDetailResponse,
|
||||
isOnlyRental: Boolean = false,
|
||||
orderType: OrderType
|
||||
) {
|
||||
AudioContentOrderConfirmDialog(
|
||||
@@ -926,9 +1106,16 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
||||
profileImageUrl = audioContent.creator.profileImageUrl,
|
||||
nickname = audioContent.creator.nickname,
|
||||
duration = audioContent.duration,
|
||||
isOnlyRental = isOnlyRental,
|
||||
orderType = orderType,
|
||||
price = audioContent.price,
|
||||
price = if (
|
||||
audioContent.purchaseOption == PurchaseOption.BOTH &&
|
||||
orderType == OrderType.RENTAL
|
||||
) {
|
||||
ceil(audioContent.price * 0.7).toInt()
|
||||
} else {
|
||||
audioContent.price
|
||||
},
|
||||
isAvailableUsePoint = binding.ivPoint.visibility == View.VISIBLE,
|
||||
confirmButtonClick = {
|
||||
startService(
|
||||
Intent(this, AudioContentPlayService::class.java).apply {
|
||||
@@ -948,7 +1135,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
||||
putExtra(
|
||||
"can",
|
||||
if (orderType == OrderType.RENTAL) {
|
||||
ceil(audioContent.price * 0.6).toInt()
|
||||
ceil(audioContent.price * 0.7).toInt()
|
||||
} else {
|
||||
audioContent.price
|
||||
}
|
||||
@@ -1011,7 +1198,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
|
||||
false
|
||||
)
|
||||
|
||||
viewModel.isLoading.value = isLoading ?: false
|
||||
viewModel.isLoading.value = isLoading == true
|
||||
|
||||
if (this@AudioContentDetailActivity.audioContentId == contentId) {
|
||||
runOnUiThread {
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.detail
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
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
|
||||
@@ -19,6 +10,7 @@ import kr.co.vividnext.sodalive.audio_content.comment.AudioContentCommentReposit
|
||||
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.Utils
|
||||
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
||||
import kr.co.vividnext.sodalive.mypage.auth.AuthRepository
|
||||
import kr.co.vividnext.sodalive.mypage.auth.AuthVerifyRequest
|
||||
@@ -139,72 +131,6 @@ class AudioContentDetailViewModel(
|
||||
)
|
||||
}
|
||||
|
||||
fun registerNotification(contentId: Long, creatorId: Long) {
|
||||
isLoading.value = true
|
||||
compositeDisposable.add(
|
||||
repository.registerNotification(
|
||||
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("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
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("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun toggleExpandDetail() {
|
||||
_isExpandDetail.value = !_isExpandDetail.value!!
|
||||
}
|
||||
@@ -371,38 +297,18 @@ class AudioContentDetailViewModel(
|
||||
)
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -2,6 +2,7 @@ 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
|
||||
|
||||
@@ -23,8 +24,8 @@ data class GetAudioContentDetailResponse(
|
||||
@SerializedName("isActivePreview") val isActivePreview: Boolean,
|
||||
@SerializedName("isAdult") val isAdult: Boolean,
|
||||
@SerializedName("isMosaic") val isMosaic: Boolean,
|
||||
@SerializedName("isOnlyRental") val isOnlyRental: Boolean,
|
||||
@SerializedName("existOrdered") val existOrdered: Boolean,
|
||||
@SerializedName("purchaseOption") val purchaseOption: PurchaseOption,
|
||||
@SerializedName("orderType") val orderType: OrderType?,
|
||||
@SerializedName("remainingTime") val remainingTime: String?,
|
||||
@SerializedName("creatorOtherContentList")
|
||||
@@ -38,7 +39,11 @@ data class GetAudioContentDetailResponse(
|
||||
@SerializedName("commentCount") val commentCount: Int,
|
||||
@SerializedName("isPin") val isPin: Boolean,
|
||||
@SerializedName("isAvailablePin") val isAvailablePin: Boolean,
|
||||
@SerializedName("creator") val creator: AudioContentCreator
|
||||
@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
|
||||
)
|
||||
|
||||
@Keep
|
||||
@@ -57,3 +62,9 @@ data class AudioContentCreator(
|
||||
@SerializedName("isFollow") var isFollow: Boolean,
|
||||
@SerializedName("isNotify") var isNotify: Boolean
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class ContentBuyer(
|
||||
@SerializedName("nickname") val nickname: String,
|
||||
@SerializedName("profileImageUrl") val profileImageUrl: String
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -1,700 +0,0 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.main
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.graphics.Rect
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
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 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.all.AudioContentNewAllActivity
|
||||
import kr.co.vividnext.sodalive.audio_content.all.AudioContentRankingAllActivity
|
||||
import kr.co.vividnext.sodalive.audio_content.all.by_theme.AudioContentAllByThemeActivity
|
||||
import kr.co.vividnext.sodalive.audio_content.curation.AudioContentCurationActivity
|
||||
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
|
||||
import kr.co.vividnext.sodalive.audio_content.main.banner.AudioContentMainBannerAdapter
|
||||
import kr.co.vividnext.sodalive.audio_content.main.banner.AudioContentMainBannerViewModel
|
||||
import kr.co.vividnext.sodalive.audio_content.main.curation.AudioContentMainCurationAdapter
|
||||
import kr.co.vividnext.sodalive.audio_content.main.curation.AudioContentMainCurationViewModel
|
||||
import kr.co.vividnext.sodalive.audio_content.main.new_content.AudioContentMainNewContentThemeAdapter
|
||||
import kr.co.vividnext.sodalive.audio_content.main.new_content.AudioContentMainNewContentViewModel
|
||||
import kr.co.vividnext.sodalive.audio_content.main.order.AudioContentMainOrderListViewModel
|
||||
import kr.co.vividnext.sodalive.audio_content.main.ranking.AudioContentMainRankingAdapter
|
||||
import kr.co.vividnext.sodalive.audio_content.main.ranking.AudioContentMainRankingViewModel
|
||||
import kr.co.vividnext.sodalive.audio_content.main.recommend_series.AudioContentMainRecommendSeriesViewModel
|
||||
import kr.co.vividnext.sodalive.audio_content.order.AudioContentOrderListActivity
|
||||
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
|
||||
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.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.databinding.FragmentAudioContentMainBinding
|
||||
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
|
||||
import kr.co.vividnext.sodalive.explorer.profile.series.UserProfileSeriesListAdapter
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import kr.co.vividnext.sodalive.mypage.alarm.AlarmListActivity
|
||||
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 recommendSeriesViewModel: AudioContentMainRecommendSeriesViewModel by inject()
|
||||
private lateinit var seriesAdapter: UserProfileSeriesListAdapter
|
||||
|
||||
private val bannerViewModel: AudioContentMainBannerViewModel by inject()
|
||||
private lateinit var bannerAdapter: AudioContentMainBannerAdapter
|
||||
|
||||
private val orderListViewModel: AudioContentMainOrderListViewModel by inject()
|
||||
private lateinit var orderListAdapter: AudioContentMainContentAdapter
|
||||
|
||||
private val newContentViewModel: AudioContentMainNewContentViewModel by inject()
|
||||
private lateinit var newContentThemeAdapter: AudioContentMainNewContentThemeAdapter
|
||||
private lateinit var newContentAdapter: AudioContentMainContentAdapter
|
||||
|
||||
private val contentRankingViewModel: AudioContentMainRankingViewModel by inject()
|
||||
private lateinit var contentRankingSortAdapter: AudioContentMainNewContentThemeAdapter
|
||||
private lateinit var contentRankingAdapter: AudioContentMainRankingAdapter
|
||||
|
||||
private val curationViewModel: AudioContentMainCurationViewModel by inject()
|
||||
private lateinit var curationAdapter: AudioContentMainCurationAdapter
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
setupView()
|
||||
|
||||
curationViewModel.getCurationList()
|
||||
bannerViewModel.getMainBannerList()
|
||||
newContentViewModel.getThemeList()
|
||||
newContentViewModel.getNewContentOfTheme("전체")
|
||||
contentRankingViewModel.getContentRanking()
|
||||
contentRankingViewModel.getContentRankingSortType()
|
||||
recommendSeriesViewModel.getRecommendSeriesList()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
setupRecommendSeries()
|
||||
setupBanner()
|
||||
setupOrderList()
|
||||
setupNewContentTheme()
|
||||
setupNewContent()
|
||||
setupContentRankingSortType()
|
||||
setupContentRanking()
|
||||
setupCuration()
|
||||
|
||||
binding.llShortPlay.setOnClickListener {
|
||||
startActivity(
|
||||
Intent(requireContext(), AudioContentAllByThemeActivity::class.java).apply {
|
||||
putExtra(Constants.EXTRA_THEME_ID, 11L)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
binding.llReviewLive.setOnClickListener {
|
||||
startActivity(
|
||||
Intent(requireContext(), AudioContentAllByThemeActivity::class.java).apply {
|
||||
putExtra(Constants.EXTRA_THEME_ID, 7L)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
binding.ivContentKeep.setOnClickListener {
|
||||
startActivity(
|
||||
Intent(
|
||||
requireContext(),
|
||||
AudioContentOrderListActivity::class.java
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
binding.ivAlarm.setOnClickListener {
|
||||
startActivity(
|
||||
Intent(
|
||||
requireActivity(),
|
||||
AlarmListActivity::class.java
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupRecommendSeries() {
|
||||
seriesAdapter = UserProfileSeriesListAdapter(
|
||||
onClickItem = {
|
||||
startActivity(
|
||||
Intent(requireContext(), SeriesDetailActivity::class.java).apply {
|
||||
putExtra(Constants.EXTRA_SERIES_ID, it)
|
||||
}
|
||||
)
|
||||
},
|
||||
onClickCreator = {
|
||||
startActivity(
|
||||
Intent(requireContext(), UserProfileActivity::class.java).apply {
|
||||
putExtra(Constants.EXTRA_USER_ID, it)
|
||||
}
|
||||
)
|
||||
},
|
||||
isVisibleCreator = true
|
||||
)
|
||||
|
||||
val recyclerView = binding.rvRecommendSeries
|
||||
recyclerView.layoutManager = LinearLayoutManager(
|
||||
requireContext(),
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
|
||||
recyclerView.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()
|
||||
}
|
||||
|
||||
seriesAdapter.itemCount - 1 -> {
|
||||
outRect.right = 0
|
||||
outRect.left = 6.7f.dpToPx().toInt()
|
||||
}
|
||||
|
||||
else -> {
|
||||
outRect.left = 6.7f.dpToPx().toInt()
|
||||
outRect.right = 6.7f.dpToPx().toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
recyclerView.adapter = seriesAdapter
|
||||
|
||||
recommendSeriesViewModel.seriesListLiveData.observe(viewLifecycleOwner) {
|
||||
seriesAdapter.addItems(it)
|
||||
binding.llRecommendSeries.visibility = if (
|
||||
seriesAdapter.itemCount <= 0 && it.isEmpty()
|
||||
) {
|
||||
View.GONE
|
||||
} else {
|
||||
View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
recommendSeriesViewModel.toastLiveData.observe(viewLifecycleOwner) {
|
||||
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
|
||||
}
|
||||
|
||||
binding.llRecommendSeriesRefresh.setOnClickListener {
|
||||
seriesAdapter.clear()
|
||||
recommendSeriesViewModel.getRecommendSeriesList()
|
||||
}
|
||||
}
|
||||
|
||||
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_3bb9f1)
|
||||
)
|
||||
.setIndicatorSliderWidth(4f.dpToPx().toInt(), 10f.dpToPx().toInt())
|
||||
.setIndicatorHeight(4f.dpToPx().toInt())
|
||||
|
||||
bannerViewModel.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)
|
||||
}
|
||||
}
|
||||
|
||||
bannerViewModel.toastLiveData.observe(viewLifecycleOwner) {
|
||||
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
orderListViewModel.orderListLiveData.observe(viewLifecycleOwner) {
|
||||
orderListAdapter.addItems(it)
|
||||
binding.llMyStash.visibility = if (
|
||||
orderListAdapter.itemCount <= 0 && it.isEmpty()
|
||||
) {
|
||||
View.GONE
|
||||
} else {
|
||||
View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
orderListViewModel.toastLiveData.observe(viewLifecycleOwner) {
|
||||
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupNewContentTheme() {
|
||||
newContentThemeAdapter = AudioContentMainNewContentThemeAdapter {
|
||||
newContentViewModel.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()
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
newContentViewModel.themeListLiveData.observe(viewLifecycleOwner) {
|
||||
binding.llNewContent.visibility = View.VISIBLE
|
||||
newContentThemeAdapter.addItems(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupNewContent() {
|
||||
binding.ivNewContentAll.setOnClickListener {
|
||||
startActivity(Intent(requireContext(), AudioContentNewAllActivity::class.java))
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
newContentAdapter.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
|
||||
|
||||
newContentViewModel.newContentListLiveData.observe(viewLifecycleOwner) {
|
||||
newContentAdapter.addItems(it)
|
||||
}
|
||||
|
||||
newContentViewModel.isLoading.observe(viewLifecycleOwner) {
|
||||
binding.pbNewContent.visibility = if (it) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
newContentViewModel.toastLiveData.observe(viewLifecycleOwner) {
|
||||
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupContentRankingSortType() {
|
||||
contentRankingSortAdapter = AudioContentMainNewContentThemeAdapter {
|
||||
contentRankingViewModel.getContentRanking(sort = it)
|
||||
}
|
||||
|
||||
binding.rvContentRankingSort.layoutManager = LinearLayoutManager(
|
||||
context,
|
||||
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()
|
||||
}
|
||||
|
||||
contentRankingSortAdapter.itemCount - 1 -> {
|
||||
outRect.left = 4f.dpToPx().toInt()
|
||||
outRect.right = 0
|
||||
}
|
||||
|
||||
else -> {
|
||||
outRect.left = 4f.dpToPx().toInt()
|
||||
outRect.right = 4f.dpToPx().toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
binding.rvContentRankingSort.adapter = contentRankingSortAdapter
|
||||
|
||||
contentRankingViewModel.contentRankingSortListLiveData.observe(viewLifecycleOwner) {
|
||||
binding.llContentRanking.visibility = View.VISIBLE
|
||||
contentRankingSortAdapter.addItems(it)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private fun setupContentRanking() {
|
||||
binding.ivContentRankingAll.setOnClickListener {
|
||||
startActivity(Intent(requireContext(), AudioContentRankingAllActivity::class.java))
|
||||
}
|
||||
|
||||
contentRankingAdapter = AudioContentMainRankingAdapter(
|
||||
width = (screenWidth * 0.66).toInt()
|
||||
) {
|
||||
startActivity(
|
||||
Intent(requireContext(), AudioContentDetailActivity::class.java).apply {
|
||||
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
binding.rvContentRanking.layoutManager = GridLayoutManager(
|
||||
context,
|
||||
3,
|
||||
GridLayoutManager.HORIZONTAL,
|
||||
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.top = 13.3f.dpToPx().toInt()
|
||||
outRect.bottom = 13.3f.dpToPx().toInt()
|
||||
outRect.left = 13.3f.dpToPx().toInt()
|
||||
outRect.right = 13.3f.dpToPx().toInt()
|
||||
}
|
||||
})
|
||||
|
||||
binding.rvContentRanking.adapter = contentRankingAdapter
|
||||
|
||||
contentRankingViewModel.contentRankingLiveData.observe(viewLifecycleOwner) {
|
||||
binding.llContentRanking.visibility = View.VISIBLE
|
||||
binding.tvDate.text = "${it.startDate}~${it.endDate}"
|
||||
contentRankingAdapter.addItems(it.items)
|
||||
}
|
||||
|
||||
contentRankingViewModel.toastLiveData.observe(viewLifecycleOwner) {
|
||||
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
onClickCurationMore = { curationId, title ->
|
||||
startActivity(
|
||||
Intent(requireContext(), AudioContentCurationActivity::class.java).apply {
|
||||
putExtra(Constants.EXTRA_AUDIO_CONTENT_CURATION_ID, curationId)
|
||||
putExtra(Constants.EXTRA_AUDIO_CONTENT_CURATION_TITLE, title)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
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.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
|
||||
) {
|
||||
curationViewModel.getCurationList()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
binding.rvCuration.adapter = curationAdapter
|
||||
|
||||
curationViewModel.curationListLiveData.observe(viewLifecycleOwner) {
|
||||
if (curationViewModel.page == 2) {
|
||||
curationAdapter.clear()
|
||||
}
|
||||
|
||||
curationAdapter.addItems(it)
|
||||
|
||||
binding.rvCuration.visibility = if (curationAdapter.itemCount <= 0 && it.isEmpty()) {
|
||||
View.GONE
|
||||
} else {
|
||||
View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
curationViewModel.isLoading.observe(viewLifecycleOwner) {
|
||||
binding.pbCuration.visibility = if (it) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
curationViewModel.toastLiveData.observe(viewLifecycleOwner) {
|
||||
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
@@ -4,13 +4,6 @@ import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import kr.co.vividnext.sodalive.settings.event.EventItem
|
||||
|
||||
@Keep
|
||||
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,
|
||||
@@ -20,7 +13,8 @@ data class GetAudioContentMainItem(
|
||||
@SerializedName("creatorProfileImageUrl") val creatorProfileImageUrl: String,
|
||||
@SerializedName("creatorNickname") val creatorNickname: String,
|
||||
@SerializedName("price") val price: Int,
|
||||
@SerializedName("duration") val duration: String
|
||||
@SerializedName("duration") val duration: String,
|
||||
@SerializedName("isPointAvailable") val isPointAvailable: Boolean
|
||||
)
|
||||
|
||||
@Keep
|
||||
@@ -39,7 +33,9 @@ data class GetAudioContentRankingItem(
|
||||
@SerializedName("price") val price: Int,
|
||||
@SerializedName("duration") val duration: String,
|
||||
@SerializedName("creatorId") val creatorId: Long,
|
||||
@SerializedName("creatorNickname") val creatorNickname: String
|
||||
@SerializedName("creatorNickname") val creatorNickname: String,
|
||||
@SerializedName("isPointAvailable") val isPointAvailable: Boolean,
|
||||
@SerializedName("creatorProfileImageUrl") val creatorProfileImageUrl: String
|
||||
)
|
||||
|
||||
@Keep
|
||||
@@ -56,6 +52,7 @@ data class GetAudioContentBannerResponse(
|
||||
@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?
|
||||
)
|
||||
|
||||
@@ -67,5 +64,8 @@ enum class AudioContentBannerType {
|
||||
CREATOR,
|
||||
|
||||
@SerializedName("LINK")
|
||||
LINK
|
||||
LINK,
|
||||
|
||||
@SerializedName("SERIES")
|
||||
SERIES
|
||||
}
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.main.curation
|
||||
|
||||
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.audio_content.main.AudioContentMainContentAdapter
|
||||
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentCurationResponse
|
||||
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
|
||||
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,
|
||||
private val onClickCurationMore: (Long, String) -> 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
|
||||
binding.ivAll.setOnClickListener { onClickCurationMore(item.curationId, item.title) }
|
||||
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.addAll(items)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
this.items.clear()
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.main.curation
|
||||
|
||||
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.GetAudioContentCurationResponse
|
||||
import kr.co.vividnext.sodalive.base.BaseViewModel
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
|
||||
class AudioContentMainCurationViewModel(
|
||||
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 _curationListLiveData = MutableLiveData<List<GetAudioContentCurationResponse>>()
|
||||
val curationListLiveData: LiveData<List<GetAudioContentCurationResponse>>
|
||||
get() = _curationListLiveData
|
||||
|
||||
var page = 1
|
||||
var isLast = false
|
||||
private val pageSize = 10
|
||||
|
||||
fun getCurationList() {
|
||||
if (!_isLoading.value!! && !isLast) {
|
||||
_isLoading.value = true
|
||||
compositeDisposable.add(
|
||||
repository.getCurationList(
|
||||
page = page,
|
||||
size = pageSize,
|
||||
token = "Bearer ${SharedPreferenceManager.token}"
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
if (it.success && it.data != null) {
|
||||
page += 1
|
||||
|
||||
if (it.data.isNotEmpty()) {
|
||||
_curationListLiveData.postValue(it.data!!)
|
||||
} else {
|
||||
_curationListLiveData.postValue(listOf())
|
||||
isLast = true
|
||||
}
|
||||
} else {
|
||||
if (it.message != null) {
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
"큐레이션을 불러오지 못했습니다. 다시 시도해 주세요.\n" +
|
||||
"계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
_isLoading.value = false
|
||||
},
|
||||
{
|
||||
_isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue(
|
||||
"큐레이션을 불러오지 못했습니다. 다시 시도해 주세요.\n" +
|
||||
"계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
page = 1
|
||||
isLast = false
|
||||
getCurationList()
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,6 @@ class AudioContentMainNewContentThemeAdapter(
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun addItems(themeList: List<String>) {
|
||||
this.selectedTheme = ""
|
||||
this.themeList.clear()
|
||||
this.themeList.addAll(themeList)
|
||||
notifyDataSetChanged()
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.main.order
|
||||
|
||||
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.SharedPreferenceManager
|
||||
|
||||
class AudioContentMainOrderListViewModel(
|
||||
private val repository: AudioContentRepository
|
||||
) : BaseViewModel() {
|
||||
private val _toastLiveData = MutableLiveData<String?>()
|
||||
val toastLiveData: LiveData<String?>
|
||||
get() = _toastLiveData
|
||||
|
||||
private var _orderListLiveData = MutableLiveData<List<GetAudioContentMainItem>>()
|
||||
val orderListLiveData: LiveData<List<GetAudioContentMainItem>>
|
||||
get() = _orderListLiveData
|
||||
|
||||
fun getOrderList() {
|
||||
compositeDisposable.add(
|
||||
repository.getMainOrderList(token = "Bearer ${SharedPreferenceManager.token}")
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
if (it.success && it.data != null) {
|
||||
_orderListLiveData.postValue(it.data!!)
|
||||
} else {
|
||||
if (it.message != null) {
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
"주문정보를 불러오지 못했습니다. 다시 시도해 주세요.\n" +
|
||||
"계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue(
|
||||
"주문정보를 불러오지 못했습니다. 다시 시도해 주세요.\n" +
|
||||
"계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.main.ranking
|
||||
|
||||
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.GetAudioContentRanking
|
||||
import kr.co.vividnext.sodalive.base.BaseViewModel
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
|
||||
class AudioContentMainRankingViewModel(
|
||||
private val repository: AudioContentRepository
|
||||
) : BaseViewModel() {
|
||||
private val _toastLiveData = MutableLiveData<String?>()
|
||||
val toastLiveData: LiveData<String?>
|
||||
get() = _toastLiveData
|
||||
|
||||
private var _contentRankingSortListLiveData = MutableLiveData<List<String>>()
|
||||
val contentRankingSortListLiveData: LiveData<List<String>>
|
||||
get() = _contentRankingSortListLiveData
|
||||
|
||||
private var _contentRankingLiveData = MutableLiveData<GetAudioContentRanking>()
|
||||
val contentRankingLiveData: LiveData<GetAudioContentRanking>
|
||||
get() = _contentRankingLiveData
|
||||
|
||||
fun getContentRankingSortType() {
|
||||
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(
|
||||
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun getContentRanking(sort: String = "매출") {
|
||||
compositeDisposable.add(
|
||||
repository.getContentRanking(
|
||||
page = 1,
|
||||
size = 12,
|
||||
sortType = sort,
|
||||
token = "Bearer ${SharedPreferenceManager.token}"
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
if (it.success && it.data != null) {
|
||||
_contentRankingLiveData.value = it.data!!
|
||||
} else {
|
||||
if (it.message != null) {
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.main.recommend_series
|
||||
|
||||
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.series.GetSeriesListResponse
|
||||
import kr.co.vividnext.sodalive.audio_content.series.SeriesRepository
|
||||
import kr.co.vividnext.sodalive.base.BaseViewModel
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
|
||||
class AudioContentMainRecommendSeriesViewModel(
|
||||
private val repository: SeriesRepository
|
||||
) : BaseViewModel() {
|
||||
private val _toastLiveData = MutableLiveData<String?>()
|
||||
val toastLiveData: LiveData<String?>
|
||||
get() = _toastLiveData
|
||||
|
||||
private var _seriesListLiveData = MutableLiveData<List<GetSeriesListResponse.SeriesListItem>>()
|
||||
val seriesListLiveData: LiveData<List<GetSeriesListResponse.SeriesListItem>>
|
||||
get() = _seriesListLiveData
|
||||
|
||||
fun getRecommendSeriesList() {
|
||||
compositeDisposable.add(
|
||||
repository
|
||||
.getRecommendSeriesList(token = "Bearer ${SharedPreferenceManager.token}")
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
if (it.success && it.data != null) {
|
||||
_seriesListLiveData.value = it.data!!
|
||||
} else {
|
||||
if (it.message != null) {
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
"추천 시리즈를 불러오지 못했습니다. 다시 시도해 주세요.\n" +
|
||||
"계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue(
|
||||
"추천 시리즈를 불러오지 못했습니다. 다시 시도해 주세요.\n" +
|
||||
"계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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,6 +22,7 @@ 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.databinding.ActivityAudioContentModifyBinding
|
||||
@@ -33,36 +36,7 @@ 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)
|
||||
@@ -82,24 +56,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 +115,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 +155,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 +176,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
|
||||
@@ -219,8 +239,8 @@ 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_13181b
|
||||
@@ -284,5 +304,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,6 +46,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 +58,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 +79,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,10 +100,12 @@ 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)
|
||||
@@ -125,14 +142,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,
|
||||
|
||||
@@ -8,6 +8,8 @@ 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
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.order
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
@@ -15,8 +16,8 @@ import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.databinding.DialogAudioContentOrderConfirmBinding
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
||||
import kotlin.math.ceil
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
class AudioContentOrderConfirmDialog(
|
||||
activity: Activity,
|
||||
layoutInflater: LayoutInflater,
|
||||
@@ -26,9 +27,9 @@ class AudioContentOrderConfirmDialog(
|
||||
profileImageUrl: String,
|
||||
nickname: String,
|
||||
duration: String,
|
||||
isOnlyRental: Boolean,
|
||||
orderType: OrderType,
|
||||
price: Int,
|
||||
isAvailableUsePoint: Boolean,
|
||||
confirmButtonClick: () -> Unit,
|
||||
) {
|
||||
|
||||
@@ -62,19 +63,51 @@ class AudioContentOrderConfirmDialog(
|
||||
|
||||
dialogView.tvDuration.text = duration
|
||||
|
||||
if (SharedPreferenceManager.userId == 17958L) {
|
||||
dialogView.ivCan.visibility = View.GONE
|
||||
dialogView.tvPrice.text = if (orderType == OrderType.RENTAL && !isOnlyRental) {
|
||||
"${(ceil(price * 0.6).toInt() * 110).moneyFormat()}원"
|
||||
val maxUsablePoint = if (orderType == OrderType.RENTAL && isAvailableUsePoint) {
|
||||
price * 10
|
||||
} else {
|
||||
"${(price * 110).moneyFormat()}원"
|
||||
0
|
||||
}
|
||||
|
||||
val totalAvailablePoint = if (orderType == OrderType.RENTAL && isAvailableUsePoint) {
|
||||
SharedPreferenceManager.point
|
||||
} else {
|
||||
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 = "${(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.tvPrice.text = if (orderType == OrderType.RENTAL && !isOnlyRental) {
|
||||
ceil(price * 0.6).toInt().moneyFormat()
|
||||
dialogView.tvCan.visibility = View.VISIBLE
|
||||
dialogView.tvCan.text = remainingCan.moneyFormat()
|
||||
} else {
|
||||
price.moneyFormat()
|
||||
dialogView.ivCan.visibility = View.GONE
|
||||
dialogView.tvCan.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,9 +119,9 @@ class AudioContentOrderConfirmDialog(
|
||||
}
|
||||
} else {
|
||||
dialogView.tvNotice.text = if (orderType == OrderType.RENTAL) {
|
||||
"콘텐츠를 대여하시겠습니까?\n아래 캔이 차감됩니다."
|
||||
"콘텐츠를 대여하시겠습니까?\n아래 금액이 차감됩니다."
|
||||
} else {
|
||||
"콘텐츠를 소장하시겠습니까?\n아래 캔이 차감됩니다."
|
||||
"콘텐츠를 소장하시겠습니까?\n아래 금액이 차감됩니다."
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import kotlin.math.ceil
|
||||
|
||||
class AudioContentOrderFragment(
|
||||
private val price: Int,
|
||||
private val isOnlyRental: Boolean,
|
||||
private val onClickRental: () -> Unit,
|
||||
private val onClickKeep: () -> Unit
|
||||
) : BottomSheetDialogFragment() {
|
||||
@@ -43,20 +42,12 @@ class AudioContentOrderFragment(
|
||||
binding.ivRentalCan.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
if (isOnlyRental) {
|
||||
if (SharedPreferenceManager.userId == 17958L) {
|
||||
binding.tvRental.text = "${(price * 110).moneyFormat()}원"
|
||||
} else {
|
||||
binding.tvRental.text = price.moneyFormat()
|
||||
}
|
||||
binding.rlKeep.visibility = View.GONE
|
||||
} else {
|
||||
if (SharedPreferenceManager.userId == 17958L) {
|
||||
binding.tvKeep.text = "${(price * 110).moneyFormat()}원"
|
||||
binding.tvRental.text = "${(ceil(price * 0.6).toInt() * 110).moneyFormat()}원"
|
||||
binding.tvRental.text = "${(ceil(price * 0.7).toInt() * 110).moneyFormat()}원"
|
||||
} else {
|
||||
binding.tvKeep.text = price.moneyFormat()
|
||||
binding.tvRental.text = ceil(price * 0.6).toInt().moneyFormat()
|
||||
binding.tvRental.text = ceil(price * 0.7).toInt().moneyFormat()
|
||||
}
|
||||
|
||||
binding.rlKeep.visibility = View.VISIBLE
|
||||
@@ -64,7 +55,6 @@ class AudioContentOrderFragment(
|
||||
onClickKeep()
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
binding.llRental.setOnClickListener {
|
||||
onClickRental()
|
||||
|
||||
@@ -8,124 +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)
|
||||
setupToolbar()
|
||||
}
|
||||
|
||||
private fun setupToolbar() {
|
||||
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.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(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()
|
||||
}
|
||||
|
||||
viewModel.totalCount.observe(this) {
|
||||
binding.tvTotalCount.text = "$it"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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(),
|
||||
"플레이어를 실행하지 못했습니다.\n다시 시도해 주세요.",
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,441 @@
|
||||
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.main.MainActivity
|
||||
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, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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!!
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.player
|
||||
|
||||
import kr.co.vividnext.sodalive.audio_content.playlist.detail.AudioContentPlaylistContent
|
||||
|
||||
class AudioContentPlaylistManager(private val playlist: List<AudioContentPlaylistContent>) {
|
||||
private var currentIndex = -1
|
||||
|
||||
fun moveToNext(): AudioContentPlaylistContent? {
|
||||
if (playlist.isNotEmpty()) {
|
||||
currentIndex = if (currentIndex + 1 >= playlist.size) 0 else currentIndex + 1
|
||||
return playlist[currentIndex]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun moveToPrevious(): AudioContentPlaylistContent? {
|
||||
if (playlist.isNotEmpty()) {
|
||||
currentIndex = if (currentIndex - 1 < 0) playlist.size - 1 else currentIndex - 1
|
||||
return playlist[currentIndex]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun findByContentId(contentId: Long): AudioContentPlaylistContent? {
|
||||
if (playlist.isNotEmpty()) {
|
||||
currentIndex = playlist.indexOfFirst { it.id == contentId }
|
||||
return playlist[currentIndex]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun hasNextContent(): Boolean {
|
||||
return currentIndex + 1 < playlist.size
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.player
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
@Keep
|
||||
data class GenerateUrlResponse(
|
||||
@SerializedName("contentUrl") val contentUrl: String
|
||||
)
|
||||
@@ -0,0 +1,65 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.playlist
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.load
|
||||
import coil.transform.RoundedCornersTransformation
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.databinding.ItemPlaylistListBinding
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
|
||||
class AudioContentPlaylistListAdapter(
|
||||
private val onClickItem: (Long) -> Unit
|
||||
) : RecyclerView.Adapter<AudioContentPlaylistListAdapter.ViewHolder>() {
|
||||
|
||||
private val items = mutableListOf<GetPlaylistsItem>()
|
||||
|
||||
inner class ViewHolder(
|
||||
private val binding: ItemPlaylistListBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
@SuppressLint("SetTextI18n")
|
||||
fun bind(item: GetPlaylistsItem) {
|
||||
binding.ivCover.load(item.coverImageUrl) {
|
||||
crossfade(true)
|
||||
placeholder(R.drawable.bg_placeholder)
|
||||
transformations(RoundedCornersTransformation(5.3f.dpToPx()))
|
||||
}
|
||||
|
||||
binding.tvTitle.text = item.title
|
||||
binding.tvContentCount.text = "총 ${item.contentCount}개"
|
||||
|
||||
if (item.desc.isNotBlank()) {
|
||||
binding.tvDesc.text = item.desc
|
||||
binding.tvDesc.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.tvDesc.visibility = View.GONE
|
||||
}
|
||||
|
||||
binding.root.setOnClickListener { onClickItem(item.id) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
|
||||
ItemPlaylistListBinding.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 updateItems(items: List<GetPlaylistsItem>) {
|
||||
this.items.clear()
|
||||
this.items.addAll(items)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.playlist
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.graphics.Rect
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity.RESULT_OK
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kr.co.vividnext.sodalive.audio_content.playlist.create.AudioContentPlaylistCreateActivity
|
||||
import kr.co.vividnext.sodalive.audio_content.playlist.detail.AudioContentPlaylistDetailActivity
|
||||
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.FragmentAudioContentPlaylistListBinding
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
class AudioContentPlaylistListFragment : BaseFragment<FragmentAudioContentPlaylistListBinding>(
|
||||
FragmentAudioContentPlaylistListBinding::inflate
|
||||
) {
|
||||
|
||||
private val viewModel: AudioContentPlaylistListViewModel by inject()
|
||||
|
||||
private lateinit var loadingDialog: LoadingDialog
|
||||
private lateinit var adapter: AudioContentPlaylistListAdapter
|
||||
|
||||
private val createOrUpdatePlaylistResult = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
if (result.resultCode == RESULT_OK) {
|
||||
viewModel.getPlaylistList()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
setupView()
|
||||
bindData()
|
||||
viewModel.getPlaylistList()
|
||||
}
|
||||
|
||||
private fun setupView() {
|
||||
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
|
||||
adapter = AudioContentPlaylistListAdapter { playlistId ->
|
||||
createOrUpdatePlaylistResult.launch(
|
||||
Intent(requireContext(), AudioContentPlaylistDetailActivity::class.java).apply {
|
||||
putExtra(
|
||||
Constants.EXTRA_AUDIO_CONTENT_PLAYLIST_ID,
|
||||
playlistId
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val recyclerView = binding.rvPlaylistList
|
||||
recyclerView.setHasFixedSize(true)
|
||||
recyclerView.layoutManager = LinearLayoutManager(
|
||||
activity,
|
||||
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 = 5f.dpToPx().toInt()
|
||||
}
|
||||
|
||||
adapter.itemCount - 1 -> {
|
||||
outRect.top = 5f.dpToPx().toInt()
|
||||
outRect.bottom = 0
|
||||
}
|
||||
|
||||
else -> {
|
||||
outRect.top = 5f.dpToPx().toInt()
|
||||
outRect.bottom = 5f.dpToPx().toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
recyclerView.adapter = adapter
|
||||
|
||||
binding.tvCreatePlaylist.setOnClickListener {
|
||||
createOrUpdatePlaylistResult.launch(
|
||||
Intent(
|
||||
requireContext(),
|
||||
AudioContentPlaylistCreateActivity::class.java
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private fun bindData() {
|
||||
viewModel.isLoading.observe(viewLifecycleOwner) {
|
||||
if (it) {
|
||||
loadingDialog.show(screenWidth)
|
||||
} else {
|
||||
loadingDialog.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.toastLiveData.observe(viewLifecycleOwner) {
|
||||
it?.let { showToast(it) }
|
||||
}
|
||||
|
||||
viewModel.totalCountLiveData.observe(viewLifecycleOwner) {
|
||||
binding.tvTotalCount.text = "${it}개"
|
||||
}
|
||||
|
||||
viewModel.playlistLiveData.observe(viewLifecycleOwner) {
|
||||
if (it.isEmpty()) {
|
||||
binding.tvTotalCount.visibility = View.GONE
|
||||
binding.rvPlaylistList.visibility = View.GONE
|
||||
binding.llNoPlaylist.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.llNoPlaylist.visibility = View.GONE
|
||||
binding.tvTotalCount.visibility = View.VISIBLE
|
||||
binding.rvPlaylistList.visibility = View.VISIBLE
|
||||
|
||||
adapter.updateItems(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,52 +1,58 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.main.banner
|
||||
package kr.co.vividnext.sodalive.audio_content.playlist
|
||||
|
||||
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.GetAudioContentBannerResponse
|
||||
import kr.co.vividnext.sodalive.base.BaseViewModel
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
|
||||
class AudioContentMainBannerViewModel(
|
||||
private val repository: AudioContentRepository
|
||||
class AudioContentPlaylistListViewModel(
|
||||
private val repository: AudioContentPlaylistRepository
|
||||
) : BaseViewModel() {
|
||||
private val _toastLiveData = MutableLiveData<String?>()
|
||||
val toastLiveData: LiveData<String?>
|
||||
get() = _toastLiveData
|
||||
|
||||
private var _bannerLiveData = MutableLiveData<List<GetAudioContentBannerResponse>>()
|
||||
val bannerLiveData: LiveData<List<GetAudioContentBannerResponse>>
|
||||
get() = _bannerLiveData
|
||||
private var _isLoading = MutableLiveData(false)
|
||||
val isLoading: LiveData<Boolean>
|
||||
get() = _isLoading
|
||||
|
||||
fun getMainBannerList() {
|
||||
private val _totalCountLiveData = MutableLiveData<Int>()
|
||||
val totalCountLiveData: LiveData<Int>
|
||||
get() = _totalCountLiveData
|
||||
|
||||
private val _playlistLiveData = MutableLiveData<List<GetPlaylistsItem>>()
|
||||
val playlistLiveData: LiveData<List<GetPlaylistsItem>>
|
||||
get() = _playlistLiveData
|
||||
|
||||
fun getPlaylistList() {
|
||||
_isLoading.value = true
|
||||
compositeDisposable.add(
|
||||
repository.getMainBannerList(token = "Bearer ${SharedPreferenceManager.token}")
|
||||
repository.getPlaylistList(token = "Bearer ${SharedPreferenceManager.token}")
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
_isLoading.value = false
|
||||
if (it.success && it.data != null) {
|
||||
_bannerLiveData.postValue(it.data!!)
|
||||
_totalCountLiveData.value = it.data.totalCount
|
||||
_playlistLiveData.value = it.data.items
|
||||
} else {
|
||||
if (it.message != null) {
|
||||
_toastLiveData.postValue(it.message)
|
||||
} else {
|
||||
_toastLiveData.postValue(
|
||||
"배너를 불러오지 못했습니다. 다시 시도해 주세요.\n" +
|
||||
"계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
_isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue(
|
||||
"배너를 불러오지 못했습니다. 다시 시도해 주세요.\n" +
|
||||
"계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||
)
|
||||
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,39 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.playlist
|
||||
|
||||
import kr.co.vividnext.sodalive.audio_content.playlist.create.CreatePlaylistRequest
|
||||
import kr.co.vividnext.sodalive.audio_content.playlist.modify.UpdatePlaylistRequest
|
||||
|
||||
class AudioContentPlaylistRepository(private val api: PlaylistApi) {
|
||||
fun getPlaylistList(token: String) = api.getPlaylistList(authHeader = token)
|
||||
|
||||
fun getPlaylistDetail(
|
||||
playlistId: Long,
|
||||
token: String
|
||||
) = api.getPlaylistDetail(
|
||||
id = playlistId,
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
fun createPlaylist(
|
||||
request: CreatePlaylistRequest,
|
||||
token: String
|
||||
) = api.createPlaylist(request, token)
|
||||
|
||||
fun deletePlaylist(
|
||||
playlistId: Long,
|
||||
token: String
|
||||
) = api.deletePlaylist(
|
||||
id = playlistId,
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
fun updatePlaylist(
|
||||
playlistId: Long,
|
||||
request: UpdatePlaylistRequest,
|
||||
token: String
|
||||
) = api.updatePlaylist(
|
||||
id = playlistId,
|
||||
request = request,
|
||||
authHeader = token
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.playlist
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
@Keep
|
||||
data class GetPlaylistsResponse(
|
||||
@SerializedName("totalCount") val totalCount: Int,
|
||||
@SerializedName("items") val items: List<GetPlaylistsItem>
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class GetPlaylistsItem(
|
||||
@SerializedName("id") val id: Long,
|
||||
@SerializedName("title") val title: String,
|
||||
@SerializedName("desc") val desc: String,
|
||||
@SerializedName("contentCount") val contentCount: Int,
|
||||
@SerializedName("coverImageUrl") val coverImageUrl: String
|
||||
)
|
||||
@@ -0,0 +1,46 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.playlist
|
||||
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import kr.co.vividnext.sodalive.audio_content.playlist.create.CreatePlaylistRequest
|
||||
import kr.co.vividnext.sodalive.audio_content.playlist.detail.GetPlaylistDetailResponse
|
||||
import kr.co.vividnext.sodalive.audio_content.playlist.modify.UpdatePlaylistRequest
|
||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.DELETE
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.PUT
|
||||
import retrofit2.http.Path
|
||||
|
||||
interface PlaylistApi {
|
||||
@GET("/audio-content/playlist")
|
||||
fun getPlaylistList(
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<GetPlaylistsResponse>>
|
||||
|
||||
@GET("/audio-content/playlist/{id}")
|
||||
fun getPlaylistDetail(
|
||||
@Path("id") id: Long,
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<GetPlaylistDetailResponse>>
|
||||
|
||||
@POST("/audio-content/playlist")
|
||||
fun createPlaylist(
|
||||
@Body request: CreatePlaylistRequest,
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<Any>>
|
||||
|
||||
@DELETE("/audio-content/playlist/{id}")
|
||||
fun deletePlaylist(
|
||||
@Path("id") id: Long,
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<Any>>
|
||||
|
||||
@PUT("/audio-content/playlist/{id}")
|
||||
fun updatePlaylist(
|
||||
@Path("id") id: Long,
|
||||
@Body request: UpdatePlaylistRequest,
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<Any>>
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.playlist.create
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Service
|
||||
import android.graphics.Rect
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.jakewharton.rxbinding4.widget.textChanges
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kr.co.vividnext.sodalive.audio_content.playlist.create.add_content.PlaylistAddContentDialogFragment
|
||||
import kr.co.vividnext.sodalive.audio_content.playlist.detail.AudioContentPlaylistContent
|
||||
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentPlaylistCreateBinding
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
class AudioContentPlaylistCreateActivity : BaseActivity<ActivityAudioContentPlaylistCreateBinding>(
|
||||
ActivityAudioContentPlaylistCreateBinding::inflate
|
||||
) {
|
||||
|
||||
private val viewModel: AudioContentPlaylistCreateViewModel by inject()
|
||||
|
||||
private lateinit var imm: InputMethodManager
|
||||
private lateinit var loadingDialog: LoadingDialog
|
||||
private lateinit var adapter: AudioContentPlaylistCreateContentAdapter
|
||||
|
||||
private val addContentDialogFragment: PlaylistAddContentDialogFragment by lazy {
|
||||
PlaylistAddContentDialogFragment(screenWidth, viewModel.contentList) { item, isChecked ->
|
||||
when {
|
||||
isChecked -> {
|
||||
viewModel.addContent(
|
||||
AudioContentPlaylistContent(
|
||||
id = item.contentId,
|
||||
title = item.title,
|
||||
category = item.themeStr,
|
||||
coverUrl = item.coverImageUrl,
|
||||
duration = item.duration ?: "00:00:00",
|
||||
creatorNickname = item.creatorNickname,
|
||||
creatorProfileUrl = ""
|
||||
)
|
||||
)
|
||||
return@PlaylistAddContentDialogFragment true
|
||||
}
|
||||
|
||||
!isChecked -> {
|
||||
viewModel.removeContentId(item.contentId)
|
||||
return@PlaylistAddContentDialogFragment true
|
||||
}
|
||||
|
||||
else -> {
|
||||
return@PlaylistAddContentDialogFragment false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
imm = getSystemService(
|
||||
Service.INPUT_METHOD_SERVICE
|
||||
) as InputMethodManager
|
||||
|
||||
bindData()
|
||||
}
|
||||
|
||||
override fun setupView() {
|
||||
binding.tvBack.text = "새 재생목록 만들기"
|
||||
binding.tvBack.setOnClickListener { finish() }
|
||||
|
||||
loadingDialog = LoadingDialog(this, layoutInflater)
|
||||
|
||||
binding.tvAddContent.setOnClickListener {
|
||||
if (addContentDialogFragment.isAdded) return@setOnClickListener
|
||||
addContentDialogFragment.show(supportFragmentManager, addContentDialogFragment.tag)
|
||||
}
|
||||
|
||||
binding.tvSave.setOnClickListener {
|
||||
viewModel.savePlaylist {
|
||||
setResult(RESULT_OK)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
adapter = AudioContentPlaylistCreateContentAdapter()
|
||||
|
||||
val recyclerView = binding.rvPlaylistContent
|
||||
recyclerView.layoutManager = LinearLayoutManager(
|
||||
this,
|
||||
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 = 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
recyclerView.adapter = adapter
|
||||
}
|
||||
|
||||
private fun bindData() {
|
||||
viewModel.toastLiveData.observe(this) {
|
||||
it?.let { showToast(it) }
|
||||
}
|
||||
|
||||
viewModel.isLoading.observe(this) {
|
||||
if (it) {
|
||||
loadingDialog.show(screenWidth, "")
|
||||
} else {
|
||||
loadingDialog.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.contentListLiveData.observe(this) {
|
||||
adapter.updateItems(it)
|
||||
}
|
||||
|
||||
compositeDisposable.add(
|
||||
binding.etTitle.textChanges()
|
||||
.map { it.toString() }
|
||||
.distinctUntilChanged()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
if (it.length > 30) {
|
||||
val truncated = it.take(30)
|
||||
binding.etTitle.setText(truncated)
|
||||
binding.etTitle.setSelection(truncated.length)
|
||||
setTitle(truncated)
|
||||
} else {
|
||||
setTitle(it)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
compositeDisposable.add(
|
||||
binding.etDesc.textChanges()
|
||||
.map { it.toString() }
|
||||
.distinctUntilChanged()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
if (it.length > 40) {
|
||||
val truncated = it.take(40)
|
||||
binding.etDesc.setText(truncated)
|
||||
binding.etDesc.setSelection(truncated.length)
|
||||
setDesc(truncated)
|
||||
} else {
|
||||
setDesc(it)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private fun setTitle(title: String) {
|
||||
binding.tvTitleLength.text = "${title.length}/30"
|
||||
viewModel.title = title
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private fun setDesc(desc: String) {
|
||||
binding.tvDescLength.text = "${desc.length}/40"
|
||||
viewModel.desc = desc
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.playlist.create
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
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.playlist.detail.AudioContentPlaylistContent
|
||||
import kr.co.vividnext.sodalive.databinding.ItemPlaylistCreateContentBinding
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
|
||||
class AudioContentPlaylistCreateContentAdapter :
|
||||
RecyclerView.Adapter<AudioContentPlaylistCreateContentAdapter.ViewHolder>() {
|
||||
private val items = mutableListOf<AudioContentPlaylistContent>()
|
||||
|
||||
inner class ViewHolder(
|
||||
private val binding: ItemPlaylistCreateContentBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(item: AudioContentPlaylistContent) {
|
||||
binding.ivCover.load(item.coverUrl) {
|
||||
crossfade(true)
|
||||
placeholder(R.drawable.bg_placeholder)
|
||||
transformations(RoundedCornersTransformation(5.3f.dpToPx()))
|
||||
}
|
||||
|
||||
binding.tvTitle.text = item.title
|
||||
binding.tvTheme.text = item.category
|
||||
binding.tvDuration.text = item.duration
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
|
||||
ItemPlaylistCreateContentBinding.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 updateItems(items: List<AudioContentPlaylistContent>) {
|
||||
this.items.clear()
|
||||
this.items.addAll(items)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.playlist.create
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kr.co.vividnext.sodalive.audio_content.playlist.AudioContentPlaylistRepository
|
||||
import kr.co.vividnext.sodalive.audio_content.playlist.detail.AudioContentPlaylistContent
|
||||
import kr.co.vividnext.sodalive.base.BaseViewModel
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
|
||||
class AudioContentPlaylistCreateViewModel(
|
||||
private val repository: AudioContentPlaylistRepository
|
||||
) : BaseViewModel() {
|
||||
private val _toastLiveData = MutableLiveData<String?>()
|
||||
val toastLiveData: LiveData<String?>
|
||||
get() = _toastLiveData
|
||||
|
||||
private val _contentListLiveData = MutableLiveData<List<AudioContentPlaylistContent>>()
|
||||
val contentListLiveData: LiveData<List<AudioContentPlaylistContent>>
|
||||
get() = _contentListLiveData
|
||||
|
||||
private var _isLoading = MutableLiveData(false)
|
||||
val isLoading: LiveData<Boolean>
|
||||
get() = _isLoading
|
||||
|
||||
val contentList = mutableListOf<AudioContentPlaylistContent>()
|
||||
|
||||
var title: String = ""
|
||||
var desc: String = ""
|
||||
|
||||
fun addContent(item: AudioContentPlaylistContent) {
|
||||
contentList.add(item)
|
||||
_contentListLiveData.value = contentList
|
||||
}
|
||||
|
||||
fun removeContentId(id: Long) {
|
||||
contentList.removeAll { it.id == id }
|
||||
_contentListLiveData.value = contentList
|
||||
}
|
||||
|
||||
fun savePlaylist(onSuccess: () -> Unit) {
|
||||
if (validate()) {
|
||||
_isLoading.value = true
|
||||
val contentIdAndOrderList = contentList.mapIndexed { index, item ->
|
||||
PlaylistContentIdAndOrder(item.id, index + 1)
|
||||
}
|
||||
|
||||
compositeDisposable.add(
|
||||
repository.createPlaylist(
|
||||
request = CreatePlaylistRequest(
|
||||
title = title,
|
||||
desc = desc,
|
||||
contentIdAndOrderList = contentIdAndOrderList
|
||||
),
|
||||
token = "Bearer ${SharedPreferenceManager.token}"
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
_isLoading.value = false
|
||||
if (it.success) {
|
||||
onSuccess()
|
||||
} else {
|
||||
if (it.message != null) {
|
||||
_toastLiveData.value = it.message
|
||||
} else {
|
||||
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
_isLoading.value = false
|
||||
if (it.message != null) {
|
||||
_toastLiveData.value = it.message
|
||||
} else {
|
||||
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun validate(): Boolean {
|
||||
if (title.isBlank() || title.length < 3) {
|
||||
_toastLiveData.value = "제목을 3자 이상 입력하세요"
|
||||
return false
|
||||
}
|
||||
|
||||
if (contentList.isEmpty()) {
|
||||
_toastLiveData.value = "콘텐츠를 1개 이상 추가하세요"
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.playlist.create
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
@Keep
|
||||
data class CreatePlaylistRequest(
|
||||
@SerializedName("title") val title: String,
|
||||
@SerializedName("desc") val desc: String? = null,
|
||||
@SerializedName("contentIdAndOrderList")
|
||||
val contentIdAndOrderList: List<PlaylistContentIdAndOrder>
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.playlist.create
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
@Keep
|
||||
data class PlaylistContentIdAndOrder(
|
||||
@SerializedName("contentId") val contentId: Long,
|
||||
@SerializedName("order") val order: Int
|
||||
)
|
||||
@@ -0,0 +1,93 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.playlist.create.add_content
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
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.order.GetAudioContentOrderListItem
|
||||
import kr.co.vividnext.sodalive.databinding.ItemPlaylistAddContentBinding
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
|
||||
class PlaylistAddContentAdapter(
|
||||
private val selectedContentIdList: Set<Long>,
|
||||
private val onItemClick: (GetAudioContentOrderListItem, Boolean) -> Boolean
|
||||
) : RecyclerView.Adapter<PlaylistAddContentAdapter.ViewHolder>() {
|
||||
|
||||
var items = mutableListOf<GetAudioContentOrderListItem>()
|
||||
|
||||
inner class ViewHolder(
|
||||
private val context: Context,
|
||||
private val binding: ItemPlaylistAddContentBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
private var isChecked = false
|
||||
|
||||
fun bind(item: GetAudioContentOrderListItem) {
|
||||
if (selectedContentIdList.contains(item.contentId)) {
|
||||
binding.ivAdd.setImageResource(R.drawable.ic_check_blue)
|
||||
isChecked = true
|
||||
} else {
|
||||
binding.ivAdd.setImageResource(R.drawable.ic_playlist_add)
|
||||
isChecked = false
|
||||
}
|
||||
|
||||
Glide
|
||||
.with(context)
|
||||
.load(item.coverImageUrl)
|
||||
.apply(
|
||||
RequestOptions().transform(
|
||||
CenterCrop(),
|
||||
RoundedCorners(5.3f.dpToPx().toInt())
|
||||
)
|
||||
)
|
||||
.into(binding.ivCover)
|
||||
|
||||
binding.tvTitle.text = item.title
|
||||
binding.tvTheme.text = item.themeStr
|
||||
binding.tvDuration.text = item.duration
|
||||
|
||||
binding.root.setOnClickListener {
|
||||
isChecked = !isChecked
|
||||
|
||||
if (onItemClick(item, isChecked)) {
|
||||
binding.ivAdd.setImageResource(
|
||||
if (isChecked) {
|
||||
R.drawable.ic_check_blue
|
||||
} else {
|
||||
R.drawable.ic_playlist_add
|
||||
}
|
||||
)
|
||||
} else {
|
||||
isChecked = !isChecked
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
|
||||
parent.context,
|
||||
ItemPlaylistAddContentBinding.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 clear() {
|
||||
items.clear()
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.playlist.create.add_content
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Dialog
|
||||
import android.content.DialogInterface
|
||||
import android.graphics.Rect
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.Toast
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
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.audio_content.order.AudioContentOrderListViewModel
|
||||
import kr.co.vividnext.sodalive.audio_content.order.GetAudioContentOrderListItem
|
||||
import kr.co.vividnext.sodalive.audio_content.playlist.detail.AudioContentPlaylistContent
|
||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||
import kr.co.vividnext.sodalive.databinding.FragmentPlaylistAddContentBinding
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
class PlaylistAddContentDialogFragment(
|
||||
private val screenWidth: Int,
|
||||
private val selectedContentList: List<AudioContentPlaylistContent>,
|
||||
private val onItemClick: (GetAudioContentOrderListItem, Boolean) -> Boolean
|
||||
) : BottomSheetDialogFragment() {
|
||||
|
||||
private lateinit var binding: FragmentPlaylistAddContentBinding
|
||||
private lateinit var adapter: PlaylistAddContentAdapter
|
||||
private lateinit var loadingDialog: LoadingDialog
|
||||
|
||||
private val viewModel: AudioContentOrderListViewModel by inject()
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val dialog = super.onCreateDialog(savedInstanceState)
|
||||
|
||||
dialog.setOnShowListener { dialogInterface ->
|
||||
val d = dialogInterface as BottomSheetDialog
|
||||
val bottomSheet = d.findViewById<FrameLayout>(
|
||||
com.google.android.material.R.id.design_bottom_sheet
|
||||
)
|
||||
if (bottomSheet != null) {
|
||||
BottomSheetBehavior.from(bottomSheet).state = BottomSheetBehavior.STATE_EXPANDED
|
||||
}
|
||||
bottomSheet?.let {
|
||||
val behavior = BottomSheetBehavior.from(bottomSheet)
|
||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
behavior.skipCollapsed = true
|
||||
|
||||
it.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
|
||||
it.requestLayout()
|
||||
}
|
||||
}
|
||||
|
||||
return dialog
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = FragmentPlaylistAddContentBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
setupView()
|
||||
bindData()
|
||||
viewModel.getAudioContentOrderList { dismiss() }
|
||||
}
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
viewModel.page = 1
|
||||
super.onDismiss(dialog)
|
||||
}
|
||||
|
||||
private fun setupView() {
|
||||
binding.tvClose.setOnClickListener { dismiss() }
|
||||
|
||||
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
|
||||
adapter = PlaylistAddContentAdapter(
|
||||
selectedContentList.map { it.id }.toSet(),
|
||||
onItemClick
|
||||
)
|
||||
|
||||
val recyclerView = binding.rvContent
|
||||
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 = 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
recyclerView.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 {}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
recyclerView.adapter = adapter
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n", "NotifyDataSetChanged")
|
||||
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.orderList.observe(viewLifecycleOwner) {
|
||||
if (viewModel.page == 2) {
|
||||
adapter.items.clear()
|
||||
}
|
||||
|
||||
adapter.items.addAll(it)
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
viewModel.totalCount.observe(viewLifecycleOwner) {
|
||||
binding.tvContentCount.text = "${it}개"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.playlist.detail
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Rect
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.content.ContextCompat
|
||||
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.SessionToken
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
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.AudioContentPlayService
|
||||
import kr.co.vividnext.sodalive.audio_content.player.AudioContentPlayerFragment
|
||||
import kr.co.vividnext.sodalive.audio_content.player.AudioContentPlayerService
|
||||
import kr.co.vividnext.sodalive.audio_content.playlist.modify.AudioContentPlaylistModifyActivity
|
||||
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||
import kr.co.vividnext.sodalive.base.SodaDialog
|
||||
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.ActivityAudioContentPlaylistDetailBinding
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import org.koin.android.ext.android.inject
|
||||
import kotlin.random.Random
|
||||
|
||||
@UnstableApi
|
||||
class AudioContentPlaylistDetailActivity : BaseActivity<ActivityAudioContentPlaylistDetailBinding>(
|
||||
ActivityAudioContentPlaylistDetailBinding::inflate
|
||||
) {
|
||||
|
||||
private val viewModel: AudioContentPlaylistDetailViewModel by inject()
|
||||
|
||||
private lateinit var loadingDialog: LoadingDialog
|
||||
private lateinit var adapter: AudioContentPlaylistDetailAdapter
|
||||
|
||||
private var playlistId: Long = 0
|
||||
|
||||
private val modifyPlaylistResult = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
if (result.resultCode == RESULT_OK) {
|
||||
viewModel.getPlaylistDetail(playlistId)
|
||||
}
|
||||
}
|
||||
|
||||
private val contentList = mutableListOf<AudioContentPlaylistContent>()
|
||||
private var mediaController: MediaController? = null
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private val preferenceChangeListener =
|
||||
SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
|
||||
// 특정 키에 대한 값이 변경될 때 UI 업데이트
|
||||
if (key == Constants.PREF_IS_PLAYER_SERVICE_RUNNING) {
|
||||
if (sharedPreferences.getBoolean(key, false)) {
|
||||
handler.postDelayed(
|
||||
{
|
||||
initAndVisibleMiniPlayer()
|
||||
},
|
||||
1500
|
||||
)
|
||||
} else {
|
||||
deInitMiniPlayer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initAndVisibleMiniPlayer() {
|
||||
binding.clMiniPlayer.visibility = View.VISIBLE
|
||||
binding.clMiniPlayer.setOnClickListener { showPlayerFragment() }
|
||||
binding.ivStop.setOnClickListener {
|
||||
startService(
|
||||
Intent(applicationContext, AudioContentPlayerService::class.java).apply {
|
||||
action = "STOP_SERVICE"
|
||||
}
|
||||
)
|
||||
}
|
||||
connectPlayerService()
|
||||
}
|
||||
|
||||
private fun connectPlayerService() {
|
||||
val componentName = ComponentName(applicationContext, AudioContentPlayerService::class.java)
|
||||
val sessionToken = SessionToken(applicationContext, componentName)
|
||||
val mediaControllerFuture =
|
||||
MediaController.Builder(applicationContext, sessionToken).buildAsync()
|
||||
mediaControllerFuture.addListener(
|
||||
{
|
||||
mediaController = mediaControllerFuture.get()
|
||||
setupMediaController()
|
||||
updateMediaMetadata(mediaController?.mediaMetadata)
|
||||
|
||||
binding.ivPlayOrPause.setImageResource(
|
||||
if (mediaController!!.isPlaying) {
|
||||
R.drawable.ic_player_pause
|
||||
} else {
|
||||
R.drawable.ic_player_play
|
||||
}
|
||||
)
|
||||
|
||||
binding.ivPlayOrPause.setOnClickListener {
|
||||
mediaController?.let {
|
||||
if (it.playWhenReady) {
|
||||
it.pause()
|
||||
} else {
|
||||
it.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
ContextCompat.getMainExecutor(applicationContext)
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateMediaMetadata(metadata: MediaMetadata?) {
|
||||
metadata?.let {
|
||||
binding.tvPlayerTitle.text = it.title
|
||||
binding.tvPlayerNickname.text = it.artist
|
||||
|
||||
binding.ivPlayerCover.load(it.artworkUri) {
|
||||
crossfade(true)
|
||||
placeholder(R.drawable.ic_place_holder)
|
||||
transformations(RoundedCornersTransformation(4f))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupMediaController() {
|
||||
if (mediaController == null) {
|
||||
deInitMiniPlayer()
|
||||
return
|
||||
}
|
||||
|
||||
mediaController!!.addListener(object : Player.Listener {
|
||||
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||
updateMediaMetadata(mediaItem?.mediaMetadata)
|
||||
}
|
||||
|
||||
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
|
||||
binding.ivPlayOrPause.setImageResource(
|
||||
if (playWhenReady) {
|
||||
R.drawable.ic_player_pause
|
||||
} else {
|
||||
R.drawable.ic_player_play
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun deInitMiniPlayer() {
|
||||
binding.clMiniPlayer.visibility = View.GONE
|
||||
mediaController?.release()
|
||||
mediaController = null
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
playlistId = intent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_PLAYLIST_ID, 0)
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (playlistId <= 0) {
|
||||
showToast("잘못된 요청입니다.")
|
||||
finish()
|
||||
}
|
||||
|
||||
bindData()
|
||||
viewModel.getPlaylistDetail(playlistId)
|
||||
SharedPreferenceManager.registerOnSharedPreferenceChangeListener(preferenceChangeListener)
|
||||
|
||||
if (SharedPreferenceManager.isPlayerServiceRunning) {
|
||||
initAndVisibleMiniPlayer()
|
||||
} else {
|
||||
deInitMiniPlayer()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
deInitMiniPlayer()
|
||||
SharedPreferenceManager.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun showPlayerFragment(
|
||||
contentList: ArrayList<AudioContentPlaylistContent> = arrayListOf()
|
||||
) {
|
||||
val playerFragment = AudioContentPlayerFragment(screenWidth, contentList)
|
||||
playerFragment.show(supportFragmentManager, playerFragment.tag)
|
||||
}
|
||||
|
||||
override fun setupView() {
|
||||
loadingDialog = LoadingDialog(this, layoutInflater)
|
||||
adapter = AudioContentPlaylistDetailAdapter {}
|
||||
|
||||
val recyclerView = binding.rvPlaylistDetail
|
||||
recyclerView.setHasFixedSize(true)
|
||||
recyclerView.layoutManager = LinearLayoutManager(
|
||||
applicationContext,
|
||||
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.llPlay.setOnClickListener {
|
||||
startService(
|
||||
Intent(applicationContext, AudioContentPlayService::class.java).apply {
|
||||
action = AudioContentPlayService.MusicAction.STOP.name
|
||||
}
|
||||
)
|
||||
|
||||
showPlayerFragment(contentList = ArrayList(contentList))
|
||||
}
|
||||
binding.llShuffle.setOnClickListener {
|
||||
startService(
|
||||
Intent(applicationContext, AudioContentPlayService::class.java).apply {
|
||||
action = AudioContentPlayService.MusicAction.STOP.name
|
||||
}
|
||||
)
|
||||
|
||||
val shuffledList = ArrayList(contentList).apply { shuffle(Random) }
|
||||
showPlayerFragment(contentList = shuffledList)
|
||||
}
|
||||
|
||||
binding.tvBack.setOnClickListener { finish() }
|
||||
binding.ivEdit.setOnClickListener {
|
||||
modifyPlaylistResult.launch(
|
||||
Intent(
|
||||
applicationContext,
|
||||
AudioContentPlaylistModifyActivity::class.java
|
||||
).apply {
|
||||
putExtra(
|
||||
Constants.EXTRA_AUDIO_CONTENT_PLAYLIST_ID,
|
||||
playlistId
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
binding.ivMore.setOnClickListener {
|
||||
val notifyFragment = AudioContentPlaylistDetailNotifyFragment {
|
||||
SodaDialog(
|
||||
activity = this@AudioContentPlaylistDetailActivity,
|
||||
layoutInflater = layoutInflater,
|
||||
title = "재생 목록 삭제",
|
||||
desc = "'${binding.tvTitle.text}'을 삭제하시겠습니까?",
|
||||
confirmButtonTitle = "삭제",
|
||||
confirmButtonClick = {
|
||||
viewModel.deletePlaylist(playlistId = playlistId) {
|
||||
setResult(RESULT_OK)
|
||||
finish()
|
||||
}
|
||||
},
|
||||
cancelButtonTitle = "취소",
|
||||
cancelButtonClick = {}
|
||||
).show(screenWidth)
|
||||
}
|
||||
|
||||
if (notifyFragment.isAdded) return@setOnClickListener
|
||||
notifyFragment.show(supportFragmentManager, notifyFragment.tag)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
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.detailResponseLiveData.observe(this) {
|
||||
binding.tvDesc.text = it.desc
|
||||
binding.tvTitle.text = it.title
|
||||
binding.tvContentCount.text = " ${it.contentCount}개"
|
||||
binding.tvCreateDate.text = "만든 날짜 ${it.createdDate} "
|
||||
adapter.updateItems(it.contentList)
|
||||
this.contentList.clear()
|
||||
this.contentList.addAll(it.contentList)
|
||||
updateCoverImageLayout(imageUrlList = it.playlistCoverImageList)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateCoverImageLayout(imageUrlList: List<String>) {
|
||||
val imageViews = listOf(
|
||||
binding.ivCover1,
|
||||
binding.ivCover2,
|
||||
binding.ivCover3,
|
||||
binding.ivCover4,
|
||||
binding.ivCover5,
|
||||
binding.ivCover6
|
||||
)
|
||||
|
||||
imageViews.forEach { it.visibility = View.GONE }
|
||||
|
||||
when (imageUrlList.size) {
|
||||
1, 2 -> {
|
||||
setImage(imageViews[0], imageUrlList[0])
|
||||
}
|
||||
|
||||
3 -> {
|
||||
setImage(imageViews[1], imageUrlList[0])
|
||||
setImage(imageViews[2], imageUrlList[1])
|
||||
setImage(imageViews[3], imageUrlList[2])
|
||||
}
|
||||
|
||||
4 -> {
|
||||
setImage(imageViews[1], imageUrlList[0])
|
||||
setImage(imageViews[2], imageUrlList[1])
|
||||
setImage(imageViews[4], imageUrlList[2])
|
||||
setImage(imageViews[5], imageUrlList[3])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setImage(imageView: ImageView, imageUrl: String) {
|
||||
imageView.apply {
|
||||
visibility = View.VISIBLE
|
||||
this.load(imageUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.playlist.detail
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.load
|
||||
import coil.transform.RoundedCornersTransformation
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.databinding.ItemPlaylistContentBinding
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
|
||||
class AudioContentPlaylistDetailAdapter(
|
||||
private val onClickContent: (Long) -> Unit
|
||||
) : RecyclerView.Adapter<AudioContentPlaylistDetailAdapter.ViewHolder>() {
|
||||
|
||||
private val items = mutableListOf<AudioContentPlaylistContent>()
|
||||
private var currentPlayingContentId = 0L
|
||||
|
||||
inner class ViewHolder(
|
||||
private val binding: ItemPlaylistContentBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(item: AudioContentPlaylistContent) {
|
||||
binding.ivCover.load(item.coverUrl) {
|
||||
crossfade(true)
|
||||
placeholder(R.drawable.bg_placeholder)
|
||||
transformations(RoundedCornersTransformation(5.3f.dpToPx()))
|
||||
}
|
||||
|
||||
binding.tvTitle.text = item.title
|
||||
binding.tvTheme.text = item.category
|
||||
binding.tvDuration.text = item.duration
|
||||
binding.tvCreatorNickname.text = item.creatorNickname
|
||||
|
||||
if (currentPlayingContentId == item.id) {
|
||||
binding.root.setBackgroundResource(R.drawable.bg_round_corner_6_7_333bb9f1)
|
||||
} else {
|
||||
binding.root.setBackgroundResource(0)
|
||||
}
|
||||
|
||||
binding.root.setOnClickListener {
|
||||
if (currentPlayingContentId != item.id) {
|
||||
onClickContent(item.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
|
||||
ItemPlaylistContentBinding.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 updateItems(items: List<AudioContentPlaylistContent>) {
|
||||
this.items.clear()
|
||||
this.items.addAll(items)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun updateCurrentPlayingId(contentId: Long?) {
|
||||
if (contentId != null) {
|
||||
this.currentPlayingContentId = contentId
|
||||
}
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.playlist.detail
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import kr.co.vividnext.sodalive.R
|
||||
|
||||
class AudioContentPlaylistDetailNotifyFragment(
|
||||
private val onClickDelete: () -> Unit
|
||||
) : BottomSheetDialogFragment() {
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View = inflater.inflate(
|
||||
R.layout.fragment_audio_content_playlist_detail_notify,
|
||||
container,
|
||||
false
|
||||
)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
view.findViewById<TextView>(R.id.tv_delete).setOnClickListener {
|
||||
onClickDelete()
|
||||
dialog?.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,16 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.main.new_content
|
||||
package kr.co.vividnext.sodalive.audio_content.playlist.detail
|
||||
|
||||
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.audio_content.playlist.AudioContentPlaylistRepository
|
||||
import kr.co.vividnext.sodalive.base.BaseViewModel
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
|
||||
class AudioContentMainNewContentViewModel(
|
||||
private val repository: AudioContentRepository
|
||||
class AudioContentPlaylistDetailViewModel(
|
||||
private val repository: AudioContentPlaylistRepository
|
||||
) : BaseViewModel() {
|
||||
private val _toastLiveData = MutableLiveData<String?>()
|
||||
val toastLiveData: LiveData<String?>
|
||||
@@ -21,54 +20,15 @@ class AudioContentMainNewContentViewModel(
|
||||
val isLoading: LiveData<Boolean>
|
||||
get() = _isLoading
|
||||
|
||||
private var _newContentListLiveData = MutableLiveData<List<GetAudioContentMainItem>>()
|
||||
val newContentListLiveData: LiveData<List<GetAudioContentMainItem>>
|
||||
get() = _newContentListLiveData
|
||||
private val _detailResponseLiveData = MutableLiveData<GetPlaylistDetailResponse>()
|
||||
val detailResponseLiveData: LiveData<GetPlaylistDetailResponse>
|
||||
get() = _detailResponseLiveData
|
||||
|
||||
private var _themeListLiveData = MutableLiveData<List<String>>()
|
||||
val themeListLiveData: LiveData<List<String>>
|
||||
get() = _themeListLiveData
|
||||
|
||||
fun getThemeList() {
|
||||
compositeDisposable.add(
|
||||
repository.getNewContentThemeList(token = "Bearer ${SharedPreferenceManager.token}")
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
if (it.success && it.data != null) {
|
||||
val themeList = listOf("전체").union(it.data).toList()
|
||||
_themeListLiveData.postValue(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) {
|
||||
fun getPlaylistDetail(playlistId: Long) {
|
||||
_isLoading.value = true
|
||||
compositeDisposable.add(
|
||||
repository.getNewContentOfTheme(
|
||||
theme = if (theme == "전체") {
|
||||
""
|
||||
} else {
|
||||
theme
|
||||
},
|
||||
repository.getPlaylistDetail(
|
||||
playlistId = playlistId,
|
||||
token = "Bearer ${SharedPreferenceManager.token}"
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
@@ -76,7 +36,7 @@ class AudioContentMainNewContentViewModel(
|
||||
.subscribe(
|
||||
{
|
||||
if (it.success && it.data != null) {
|
||||
_newContentListLiveData.value = it.data!!
|
||||
_detailResponseLiveData.value = it.data!!
|
||||
} else {
|
||||
if (it.message != null) {
|
||||
_toastLiveData.postValue(it.message)
|
||||
@@ -86,13 +46,46 @@ class AudioContentMainNewContentViewModel(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
_isLoading.value = false
|
||||
},
|
||||
{
|
||||
_isLoading.value = false
|
||||
it.message?.let { message -> Logger.e(message) }
|
||||
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun deletePlaylist(playlistId: Long, onSuccess: () -> Unit) {
|
||||
_isLoading.value = true
|
||||
compositeDisposable.add(
|
||||
repository.deletePlaylist(
|
||||
playlistId = playlistId,
|
||||
token = "Bearer ${SharedPreferenceManager.token}"
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
if (it.success) {
|
||||
_toastLiveData.value = "삭제되었습니다."
|
||||
onSuccess()
|
||||
} 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("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,29 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.playlist.detail
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Keep
|
||||
data class GetPlaylistDetailResponse(
|
||||
@SerializedName("playlistId") val playlistId: Long,
|
||||
@SerializedName("title") val title: String,
|
||||
@SerializedName("desc") val desc: String,
|
||||
@SerializedName("createdDate") val createdDate: String,
|
||||
@SerializedName("contentCount") val contentCount: Int,
|
||||
@SerializedName("playlistCoverImageList") val playlistCoverImageList: List<String>,
|
||||
@SerializedName("contentList") val contentList: List<AudioContentPlaylistContent>
|
||||
)
|
||||
|
||||
@Keep
|
||||
@Parcelize
|
||||
data class AudioContentPlaylistContent(
|
||||
@SerializedName("id") val id: Long,
|
||||
@SerializedName("title") val title: String,
|
||||
@SerializedName("category") val category: String,
|
||||
@SerializedName("coverUrl") val coverUrl: String,
|
||||
@SerializedName("duration") val duration: String,
|
||||
@SerializedName("creatorNickname") val creatorNickname: String,
|
||||
@SerializedName("creatorProfileUrl") val creatorProfileUrl: String
|
||||
) : Parcelable
|
||||
@@ -0,0 +1,228 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.playlist.modify
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Service
|
||||
import android.graphics.Rect
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.jakewharton.rxbinding4.widget.textChanges
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kr.co.vividnext.sodalive.audio_content.playlist.create.add_content.PlaylistAddContentDialogFragment
|
||||
import kr.co.vividnext.sodalive.audio_content.playlist.detail.AudioContentPlaylistContent
|
||||
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.ActivityAudioContentPlaylistModifyBinding
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
class AudioContentPlaylistModifyActivity : BaseActivity<ActivityAudioContentPlaylistModifyBinding>(
|
||||
ActivityAudioContentPlaylistModifyBinding::inflate
|
||||
) {
|
||||
|
||||
private val viewModel: AudioContentPlaylistModifyViewModel by inject()
|
||||
|
||||
private lateinit var imm: InputMethodManager
|
||||
private lateinit var loadingDialog: LoadingDialog
|
||||
private lateinit var adapter: AudioContentPlaylistModifyContentAdapter
|
||||
|
||||
private var playlistId: Long = 0
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
|
||||
private val addContentDialogFragment: PlaylistAddContentDialogFragment by lazy {
|
||||
PlaylistAddContentDialogFragment(screenWidth, viewModel.contentList) { item, isChecked ->
|
||||
when {
|
||||
isChecked -> {
|
||||
viewModel.addContent(
|
||||
AudioContentPlaylistContent(
|
||||
id = item.contentId,
|
||||
title = item.title,
|
||||
category = item.themeStr,
|
||||
coverUrl = item.coverImageUrl,
|
||||
duration = item.duration ?: "00:00:00",
|
||||
creatorNickname = item.creatorNickname,
|
||||
creatorProfileUrl = ""
|
||||
)
|
||||
)
|
||||
return@PlaylistAddContentDialogFragment true
|
||||
}
|
||||
|
||||
!isChecked -> {
|
||||
viewModel.removeContentId(item.contentId)
|
||||
return@PlaylistAddContentDialogFragment true
|
||||
}
|
||||
|
||||
else -> {
|
||||
return@PlaylistAddContentDialogFragment false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
playlistId = intent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_PLAYLIST_ID, 0)
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (playlistId <= 0) {
|
||||
showToast("잘못된 요청입니다.")
|
||||
finish()
|
||||
}
|
||||
|
||||
imm = getSystemService(
|
||||
Service.INPUT_METHOD_SERVICE
|
||||
) as InputMethodManager
|
||||
|
||||
bindData()
|
||||
viewModel.getPlaylistDetail(playlistId)
|
||||
}
|
||||
|
||||
override fun setupView() {
|
||||
binding.tvBack.text = "재생목록 수정"
|
||||
binding.tvBack.setOnClickListener { finish() }
|
||||
|
||||
loadingDialog = LoadingDialog(this, layoutInflater)
|
||||
|
||||
binding.tvAddContent.setOnClickListener {
|
||||
hideKeyboard()
|
||||
if (addContentDialogFragment.isAdded) return@setOnClickListener
|
||||
addContentDialogFragment.show(supportFragmentManager, addContentDialogFragment.tag)
|
||||
}
|
||||
|
||||
binding.tvModify.setOnClickListener {
|
||||
hideKeyboard()
|
||||
viewModel.modifyPlaylist {
|
||||
setResult(RESULT_OK)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
adapter = AudioContentPlaylistModifyContentAdapter()
|
||||
|
||||
val recyclerView = binding.rvPlaylistContent
|
||||
recyclerView.setHasFixedSize(true)
|
||||
recyclerView.layoutManager = LinearLayoutManager(
|
||||
this,
|
||||
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 = 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
recyclerView.adapter = adapter
|
||||
}
|
||||
|
||||
private fun bindData() {
|
||||
viewModel.toastLiveData.observe(this) {
|
||||
it?.let { showToast(it) }
|
||||
}
|
||||
|
||||
viewModel.isLoading.observe(this) {
|
||||
if (it) {
|
||||
loadingDialog.show(screenWidth, "")
|
||||
} else {
|
||||
loadingDialog.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.contentListLiveData.observe(this) {
|
||||
adapter.updateItems(it)
|
||||
}
|
||||
|
||||
viewModel.detailResponseLiveData.observe(this) {
|
||||
binding.etTitle.setText(it.title)
|
||||
binding.etDesc.setText(it.desc)
|
||||
adapter.updateItems(it.contentList)
|
||||
}
|
||||
|
||||
compositeDisposable.add(
|
||||
binding.etTitle.textChanges()
|
||||
.map { it.toString() }
|
||||
.distinctUntilChanged()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
if (it.length > 30) {
|
||||
val truncated = it.take(30)
|
||||
binding.etTitle.setText(truncated)
|
||||
binding.etTitle.setSelection(truncated.length)
|
||||
setTitle(truncated)
|
||||
} else {
|
||||
setTitle(it)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
compositeDisposable.add(
|
||||
binding.etDesc.textChanges()
|
||||
.map { it.toString() }
|
||||
.distinctUntilChanged()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
if (it.length > 40) {
|
||||
val truncated = it.take(40)
|
||||
binding.etDesc.setText(truncated)
|
||||
binding.etDesc.setSelection(truncated.length)
|
||||
setDesc(truncated)
|
||||
} else {
|
||||
setDesc(it)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private fun setTitle(title: String) {
|
||||
binding.tvTitleLength.text = "${title.length}/30"
|
||||
viewModel.title = title
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private fun setDesc(desc: String) {
|
||||
binding.tvDescLength.text = "${desc.length}/40"
|
||||
viewModel.desc = desc
|
||||
}
|
||||
|
||||
private fun hideKeyboard() {
|
||||
handler.postDelayed({
|
||||
imm.hideSoftInputFromWindow(
|
||||
window.decorView.applicationWindowToken,
|
||||
InputMethodManager.HIDE_NOT_ALWAYS
|
||||
)
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.playlist.modify
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
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.playlist.detail.AudioContentPlaylistContent
|
||||
import kr.co.vividnext.sodalive.databinding.ItemPlaylistCreateContentBinding
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
|
||||
class AudioContentPlaylistModifyContentAdapter :
|
||||
RecyclerView.Adapter<AudioContentPlaylistModifyContentAdapter.ViewHolder>() {
|
||||
private val items = mutableListOf<AudioContentPlaylistContent>()
|
||||
|
||||
inner class ViewHolder(
|
||||
private val binding: ItemPlaylistCreateContentBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(item: AudioContentPlaylistContent) {
|
||||
binding.ivCover.load(item.coverUrl) {
|
||||
crossfade(true)
|
||||
placeholder(R.drawable.bg_placeholder)
|
||||
transformations(RoundedCornersTransformation(5.3f.dpToPx()))
|
||||
}
|
||||
|
||||
binding.tvTitle.text = item.title
|
||||
binding.tvTheme.text = item.category
|
||||
binding.tvDuration.text = item.duration
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
|
||||
ItemPlaylistCreateContentBinding.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 updateItems(items: List<AudioContentPlaylistContent>) {
|
||||
this.items.clear()
|
||||
this.items.addAll(items)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.playlist.modify
|
||||
|
||||
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.playlist.AudioContentPlaylistRepository
|
||||
import kr.co.vividnext.sodalive.audio_content.playlist.create.PlaylistContentIdAndOrder
|
||||
import kr.co.vividnext.sodalive.audio_content.playlist.detail.AudioContentPlaylistContent
|
||||
import kr.co.vividnext.sodalive.audio_content.playlist.detail.GetPlaylistDetailResponse
|
||||
import kr.co.vividnext.sodalive.base.BaseViewModel
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
|
||||
class AudioContentPlaylistModifyViewModel(
|
||||
private val repository: AudioContentPlaylistRepository
|
||||
) : BaseViewModel() {
|
||||
private val _toastLiveData = MutableLiveData<String?>()
|
||||
val toastLiveData: LiveData<String?>
|
||||
get() = _toastLiveData
|
||||
|
||||
private val _contentListLiveData = MutableLiveData<List<AudioContentPlaylistContent>>()
|
||||
val contentListLiveData: LiveData<List<AudioContentPlaylistContent>>
|
||||
get() = _contentListLiveData
|
||||
|
||||
private var _isLoading = MutableLiveData(false)
|
||||
val isLoading: LiveData<Boolean>
|
||||
get() = _isLoading
|
||||
|
||||
private val _detailResponseLiveData = MutableLiveData<GetPlaylistDetailResponse>()
|
||||
val detailResponseLiveData: LiveData<GetPlaylistDetailResponse>
|
||||
get() = _detailResponseLiveData
|
||||
|
||||
val contentList = mutableListOf<AudioContentPlaylistContent>()
|
||||
|
||||
var title: String = ""
|
||||
var desc: String = ""
|
||||
private var playlistId: Long = 0
|
||||
|
||||
fun addContent(item: AudioContentPlaylistContent) {
|
||||
contentList.add(item)
|
||||
_contentListLiveData.value = contentList
|
||||
}
|
||||
|
||||
fun removeContentId(id: Long) {
|
||||
contentList.removeAll { it.id == id }
|
||||
_contentListLiveData.value = contentList
|
||||
}
|
||||
|
||||
fun modifyPlaylist(onSuccess: () -> Unit) {
|
||||
if (validate()) {
|
||||
_isLoading.value = true
|
||||
val contentIdAndOrderList = contentList.mapIndexed { index, item ->
|
||||
PlaylistContentIdAndOrder(item.id, index + 1)
|
||||
}
|
||||
|
||||
val request = UpdatePlaylistRequest(
|
||||
title = if (_detailResponseLiveData.value!!.title != title) {
|
||||
title
|
||||
} else {
|
||||
null
|
||||
},
|
||||
desc = if (_detailResponseLiveData.value!!.desc != desc) {
|
||||
desc
|
||||
} else {
|
||||
null
|
||||
},
|
||||
contentIdAndOrderList = contentIdAndOrderList
|
||||
)
|
||||
|
||||
compositeDisposable.add(
|
||||
repository.updatePlaylist(
|
||||
playlistId = playlistId,
|
||||
request = request,
|
||||
token = "Bearer ${SharedPreferenceManager.token}"
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
_isLoading.value = false
|
||||
if (it.success) {
|
||||
onSuccess()
|
||||
} else {
|
||||
if (it.message != null) {
|
||||
_toastLiveData.value = it.message
|
||||
} else {
|
||||
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
_isLoading.value = false
|
||||
if (it.message != null) {
|
||||
_toastLiveData.value = it.message
|
||||
} else {
|
||||
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun validate(): Boolean {
|
||||
if (title.isBlank() || title.length < 3) {
|
||||
_toastLiveData.value = "제목을 3자 이상 입력하세요"
|
||||
return false
|
||||
}
|
||||
|
||||
if (contentList.isEmpty()) {
|
||||
_toastLiveData.value = "콘텐츠를 1개 이상 추가하세요"
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun getPlaylistDetail(playlistId: Long) {
|
||||
this.playlistId = playlistId
|
||||
_isLoading.value = true
|
||||
compositeDisposable.add(
|
||||
repository.getPlaylistDetail(
|
||||
playlistId = playlistId,
|
||||
token = "Bearer ${SharedPreferenceManager.token}"
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
if (it.success && it.data != null) {
|
||||
val data = it.data
|
||||
_detailResponseLiveData.value = data!!
|
||||
this.contentList.addAll(data.contentList)
|
||||
} 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("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.playlist.modify
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import kr.co.vividnext.sodalive.audio_content.playlist.create.PlaylistContentIdAndOrder
|
||||
|
||||
data class UpdatePlaylistRequest(
|
||||
@SerializedName("title") val title: String? = null,
|
||||
@SerializedName("desc") val desc: String? = null,
|
||||
@SerializedName("contentIdAndOrderList") val contentIdAndOrderList: List<PlaylistContentIdAndOrder> = emptyList()
|
||||
)
|
||||
@@ -4,6 +4,7 @@ import io.reactivex.rxjava3.core.Single
|
||||
import kr.co.vividnext.sodalive.audio_content.series.detail.GetSeriesContentListResponse
|
||||
import kr.co.vividnext.sodalive.audio_content.series.detail.GetSeriesDetailResponse
|
||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||
import kr.co.vividnext.sodalive.settings.ContentType
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.Path
|
||||
@@ -12,8 +13,12 @@ import retrofit2.http.Query
|
||||
interface SeriesApi {
|
||||
@GET("/audio-content/series")
|
||||
fun getSeriesList(
|
||||
@Query("creatorId") creatorId: Long,
|
||||
@Query("creatorId") creatorId: Long?,
|
||||
@Query("sortType") sortType: SeriesListAllViewModel.SeriesSortType,
|
||||
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
|
||||
@Query("contentType") contentType: ContentType,
|
||||
@Query("isOriginal") isOriginal: Boolean?,
|
||||
@Query("isCompleted") isCompleted: Boolean?,
|
||||
@Query("page") page: Int,
|
||||
@Query("size") size: Int,
|
||||
@Header("Authorization") authHeader: String
|
||||
@@ -22,20 +27,17 @@ interface SeriesApi {
|
||||
@GET("/audio-content/series/{id}")
|
||||
fun getSeriesDetail(
|
||||
@Path("id") seriesId: Long,
|
||||
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<GetSeriesDetailResponse>>
|
||||
|
||||
@GET("/audio-content/series/{id}/content")
|
||||
fun getSeriesContentList(
|
||||
@Path("id") seriesId: Long,
|
||||
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
|
||||
@Query("page") page: Int,
|
||||
@Query("size") size: Int,
|
||||
@Query("sortType") sortType: SeriesListAllViewModel.SeriesSortType,
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<GetSeriesContentListResponse>>
|
||||
|
||||
@GET("/audio-content/series/recommend")
|
||||
fun getRecommendSeriesList(
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<List<GetSeriesListResponse.SeriesListItem>>>
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import android.annotation.SuppressLint
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import android.widget.LinearLayout
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.load
|
||||
import coil.transform.CircleCropTransformation
|
||||
@@ -27,13 +27,12 @@ class SeriesListAdapter(
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
@SuppressLint("SetTextI18n")
|
||||
fun bind(item: GetSeriesListResponse.SeriesListItem) {
|
||||
val lp = binding.ivCover.layoutParams as ConstraintLayout.LayoutParams
|
||||
val lp = binding.clCover.layoutParams as LinearLayout.LayoutParams
|
||||
lp.width = itemWidth
|
||||
lp.height = itemWidth * 432 / 306
|
||||
binding.ivCover.layoutParams = lp
|
||||
binding.clCover.layoutParams = lp
|
||||
|
||||
binding.ivCover.load(item.coverImage) {
|
||||
crossfade(true)
|
||||
placeholder(R.drawable.bg_placeholder)
|
||||
transformations(RoundedCornersTransformation(5f.dpToPx()))
|
||||
}
|
||||
|
||||
@@ -9,9 +9,11 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
|
||||
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||
import kr.co.vividnext.sodalive.common.Constants
|
||||
import kr.co.vividnext.sodalive.common.DifferentSpacingItemDecoration
|
||||
import kr.co.vividnext.sodalive.common.GridSpacingItemDecoration
|
||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||
import kr.co.vividnext.sodalive.databinding.ActivitySeriesListAllBinding
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import kr.co.vividnext.sodalive.home.HomeSeriesAdapter
|
||||
import org.koin.android.ext.android.inject
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@@ -22,55 +24,65 @@ class SeriesListAllActivity : BaseActivity<ActivitySeriesListAllBinding>(
|
||||
private val viewModel: SeriesListAllViewModel by inject()
|
||||
|
||||
private lateinit var loadingDialog: LoadingDialog
|
||||
private lateinit var seriesAdapter: SeriesListAdapter
|
||||
private lateinit var seriesAdapter: HomeSeriesAdapter
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val creatorId = intent.getLongExtra(Constants.EXTRA_USER_ID, 0)
|
||||
if (creatorId <= 0) {
|
||||
Toast.makeText(applicationContext, "잘못된 요청입니다.", Toast.LENGTH_LONG).show()
|
||||
finish()
|
||||
}
|
||||
val passedCreatorId = intent.getLongExtra(Constants.EXTRA_USER_ID, 0)
|
||||
val isOriginal = intent.getBooleanExtra(Constants.EXTRA_IS_ORIGINAL, false)
|
||||
val isCompleted = intent.getBooleanExtra(Constants.EXTRA_IS_COMPLETED, false)
|
||||
|
||||
bindData()
|
||||
|
||||
viewModel.creatorId = creatorId
|
||||
viewModel.creatorId = if (passedCreatorId > 0) {
|
||||
passedCreatorId
|
||||
} else {
|
||||
null
|
||||
}
|
||||
viewModel.isCompleted = if (isCompleted) {
|
||||
true
|
||||
} else {
|
||||
null
|
||||
}
|
||||
viewModel.isOriginal = if (isOriginal) {
|
||||
true
|
||||
} else {
|
||||
null
|
||||
}
|
||||
viewModel.getSeriesList()
|
||||
}
|
||||
|
||||
override fun setupView() {
|
||||
loadingDialog = LoadingDialog(this, layoutInflater)
|
||||
binding.toolbar.tvBack.text = "시리즈 전체보기"
|
||||
binding.toolbar.tvBack.text =
|
||||
if (intent.getBooleanExtra(Constants.EXTRA_IS_COMPLETED, false)) {
|
||||
"완결 시리즈"
|
||||
} else if (intent.getBooleanExtra(Constants.EXTRA_IS_ORIGINAL, false)) {
|
||||
"오직 보이스온에서만"
|
||||
} else {
|
||||
"시리즈 전체보기"
|
||||
}
|
||||
binding.toolbar.tvBack.setOnClickListener { finish() }
|
||||
|
||||
seriesAdapter = SeriesListAdapter(
|
||||
itemWidth = ((screenWidth - (13.3 * 3)) / 3).roundToInt(),
|
||||
seriesAdapter = HomeSeriesAdapter(
|
||||
itemWidth = ((screenWidth - 24f.dpToPx() * 2 - 16f.dpToPx()) / 2f).roundToInt(),
|
||||
onClickItem = {
|
||||
startActivity(
|
||||
Intent(applicationContext, SeriesDetailActivity::class.java).apply {
|
||||
putExtra(Constants.EXTRA_SERIES_ID, it)
|
||||
}
|
||||
)
|
||||
},
|
||||
onClickCreator = {},
|
||||
isVisibleCreator = false
|
||||
}
|
||||
)
|
||||
|
||||
val spanCount = 3
|
||||
val horizontalSpacing = 20
|
||||
val verticalSpacing = 100
|
||||
val spanCount = 2
|
||||
val spacingPx = 16f.dpToPx().toInt()
|
||||
val recyclerView = binding.rvSeriesAll
|
||||
recyclerView.layoutManager = GridLayoutManager(this, spanCount)
|
||||
|
||||
recyclerView.addItemDecoration(
|
||||
DifferentSpacingItemDecoration(
|
||||
spanCount = spanCount,
|
||||
horizontalSpacing = horizontalSpacing,
|
||||
verticalSpacing = verticalSpacing,
|
||||
includeEdge = true
|
||||
|
||||
)
|
||||
GridSpacingItemDecoration(spanCount, spacingPx, true)
|
||||
)
|
||||
|
||||
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
|
||||
@@ -28,10 +28,12 @@ class SeriesListAllViewModel(private val repository: SeriesRepository) : BaseVie
|
||||
val seriesListLiveData: LiveData<List<GetSeriesListResponse.SeriesListItem>>
|
||||
get() = _seriesListLiveData
|
||||
|
||||
var creatorId = 0L
|
||||
var creatorId: Long? = null
|
||||
var isCompleted: Boolean? = null
|
||||
var isOriginal: Boolean? = null
|
||||
var isLast = false
|
||||
var page = 1
|
||||
private val size = 10
|
||||
private val size = 20
|
||||
|
||||
fun getSeriesList() {
|
||||
if (!_isLoading.value!! && !isLast) {
|
||||
@@ -40,6 +42,8 @@ class SeriesListAllViewModel(private val repository: SeriesRepository) : BaseVie
|
||||
compositeDisposable.add(
|
||||
repository.getSeriesList(
|
||||
creatorId = creatorId,
|
||||
isOriginal = isOriginal,
|
||||
isCompleted = isCompleted,
|
||||
sortType = SeriesSortType.NEWEST,
|
||||
page = page,
|
||||
size = size,
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.series
|
||||
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.settings.ContentType
|
||||
|
||||
class SeriesRepository(private val api: SeriesApi) {
|
||||
fun getSeriesList(
|
||||
creatorId: Long,
|
||||
creatorId: Long?,
|
||||
isOriginal: Boolean?,
|
||||
isCompleted: Boolean?,
|
||||
sortType: SeriesListAllViewModel.SeriesSortType,
|
||||
page: Int,
|
||||
size: Int,
|
||||
@@ -10,6 +15,10 @@ class SeriesRepository(private val api: SeriesApi) {
|
||||
) = api.getSeriesList(
|
||||
creatorId = creatorId,
|
||||
sortType = sortType,
|
||||
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
|
||||
contentType = ContentType.entries[SharedPreferenceManager.contentPreference],
|
||||
isOriginal = isOriginal,
|
||||
isCompleted = isCompleted,
|
||||
page = page - 1,
|
||||
size = size,
|
||||
authHeader = token
|
||||
@@ -17,6 +26,7 @@ class SeriesRepository(private val api: SeriesApi) {
|
||||
|
||||
fun getSeriesDetail(seriesId: Long, token: String) = api.getSeriesDetail(
|
||||
seriesId = seriesId,
|
||||
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
@@ -28,11 +38,10 @@ class SeriesRepository(private val api: SeriesApi) {
|
||||
token: String
|
||||
) = api.getSeriesContentList(
|
||||
seriesId = seriesId,
|
||||
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
|
||||
page = page - 1,
|
||||
size = size,
|
||||
sortType = sortType,
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
fun getRecommendSeriesList(token: String) = api.getRecommendSeriesList(authHeader = token)
|
||||
}
|
||||
|
||||
@@ -28,6 +28,12 @@ class SeriesContentAdapter(
|
||||
transformations(RoundedCornersTransformation(5.3f.dpToPx()))
|
||||
}
|
||||
|
||||
binding.tvPoint.visibility = if (item.isPointAvailable) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
|
||||
binding.tvTitle.text = item.title
|
||||
binding.tvDuration.text = item.duration
|
||||
|
||||
|
||||
@@ -21,5 +21,6 @@ data class GetSeriesContentListItem(
|
||||
@SerializedName("duration") val duration: String,
|
||||
@SerializedName("price") val price: Int,
|
||||
@SerializedName("isRented") var isRented: Boolean,
|
||||
@SerializedName("isOwned") var isOwned: Boolean
|
||||
@SerializedName("isOwned") var isOwned: Boolean,
|
||||
@SerializedName("isPointAvailable") var isPointAvailable: Boolean
|
||||
) : Parcelable
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.series.detail
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
@@ -21,6 +22,7 @@ import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.databinding.ActivitySeriesDetailBinding
|
||||
import kr.co.vividnext.sodalive.explorer.profile.CreatorFollowNotifyFragment
|
||||
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
@@ -162,6 +164,13 @@ class SeriesDetailActivity : BaseActivity<ActivitySeriesDetailBinding>(
|
||||
}
|
||||
|
||||
private fun setSeriesCreator(creator: GetSeriesDetailResponse.GetSeriesDetailCreator) {
|
||||
binding.llProfile.setOnClickListener {
|
||||
startActivity(
|
||||
Intent(applicationContext, UserProfileActivity::class.java).apply {
|
||||
putExtra(Constants.EXTRA_USER_ID, creator.creatorId)
|
||||
}
|
||||
)
|
||||
}
|
||||
binding.tvNickname.text = creator.nickname
|
||||
binding.ivProfile.load(creator.profileImage) {
|
||||
crossfade(true)
|
||||
|
||||
@@ -7,7 +7,6 @@ import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.chip.ChipGroup
|
||||
import com.google.android.material.shape.ShapeAppearanceModel
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.base.BaseFragment
|
||||
@@ -54,12 +53,12 @@ class SeriesDetailIntroductionFragment : BaseFragment<FragmentSeriesDetailIntrod
|
||||
|
||||
binding.tvRentalPrice.text = if (rentalMinPrice == rentalMaxPrice) {
|
||||
if (rentalMaxPrice == 0) {
|
||||
"무료(15일)"
|
||||
"무료(5일)"
|
||||
} else {
|
||||
"$rentalMaxPrice(15일)"
|
||||
"$rentalMaxPrice(5일)"
|
||||
}
|
||||
} else {
|
||||
"${if (rentalMinPrice == 0) "무료" else rentalMinPrice} ~ ${rentalMaxPrice}캔 (15일)"
|
||||
"${if (rentalMinPrice == 0) "무료" else rentalMinPrice} ~ ${rentalMaxPrice}캔 (5일)"
|
||||
}
|
||||
|
||||
binding.tvPrice.text = if (minPrice == maxPrice) {
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.series.main
|
||||
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.audio_content.series.main.by_genre.SeriesMainByGenreFragment
|
||||
import kr.co.vividnext.sodalive.audio_content.series.main.day_of_week.SeriesMainDayOfWeekFragment
|
||||
import kr.co.vividnext.sodalive.audio_content.series.main.home.SeriesMainHomeFragment
|
||||
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||
import kr.co.vividnext.sodalive.databinding.ActivitySeriesMainBinding
|
||||
|
||||
class SeriesMainActivity : BaseActivity<ActivitySeriesMainBinding>(
|
||||
ActivitySeriesMainBinding::inflate
|
||||
) {
|
||||
private var currentTab = 0
|
||||
|
||||
override fun setupView() {
|
||||
binding.toolbar.tvBack.text = "시리즈 전체보기"
|
||||
binding.toolbar.tvBack.setOnClickListener { finish() }
|
||||
|
||||
setupTabs()
|
||||
}
|
||||
|
||||
private fun setupTabs() {
|
||||
binding.tabLayout.addTab(binding.tabLayout.newTab().setText("홈"))
|
||||
binding.tabLayout.addTab(binding.tabLayout.newTab().setText("요일별"))
|
||||
binding.tabLayout.addTab(binding.tabLayout.newTab().setText("장르별"))
|
||||
|
||||
// 탭 선택 리스너 설정
|
||||
binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
||||
override fun onTabSelected(tab: TabLayout.Tab) {
|
||||
currentTab = tab.position
|
||||
showTabContent(currentTab)
|
||||
}
|
||||
|
||||
override fun onTabUnselected(tab: TabLayout.Tab) {
|
||||
// 필요한 경우 구현
|
||||
}
|
||||
|
||||
override fun onTabReselected(tab: TabLayout.Tab) {
|
||||
// 필요한 경우 구현
|
||||
}
|
||||
})
|
||||
|
||||
// 초기 탭 선택
|
||||
showTabContent(currentTab)
|
||||
}
|
||||
|
||||
private fun showTabContent(position: Int) {
|
||||
val fragmentTransaction = supportFragmentManager.beginTransaction()
|
||||
|
||||
// 기존 프래그먼트 제거
|
||||
supportFragmentManager.fragments.forEach {
|
||||
fragmentTransaction.remove(it)
|
||||
}
|
||||
|
||||
// 선택된 탭에 따라 프래그먼트 표시
|
||||
val fragment = when (position) {
|
||||
1 -> SeriesMainDayOfWeekFragment()
|
||||
2 -> SeriesMainByGenreFragment()
|
||||
else -> SeriesMainHomeFragment()
|
||||
}
|
||||
|
||||
fragmentTransaction.add(R.id.fl_container, fragment)
|
||||
fragmentTransaction.commit()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.series.main
|
||||
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import kr.co.vividnext.sodalive.audio_content.series.GetSeriesListResponse
|
||||
import kr.co.vividnext.sodalive.audio_content.series.main.by_genre.GetSeriesGenreListResponse
|
||||
import kr.co.vividnext.sodalive.audio_content.series.main.home.SeriesHomeResponse
|
||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||
import kr.co.vividnext.sodalive.home.SeriesPublishedDaysOfWeek
|
||||
import kr.co.vividnext.sodalive.settings.ContentType
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface SeriesMainApi {
|
||||
@GET("/audio-content/series/main")
|
||||
fun fetchHome(
|
||||
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
|
||||
@Query("contentType") contentType: ContentType,
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<SeriesHomeResponse>>
|
||||
|
||||
@GET("/audio-content/series/main/recommend")
|
||||
fun getRecommendSeriesList(
|
||||
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
|
||||
@Query("contentType") contentType: ContentType,
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<List<GetSeriesListResponse.SeriesListItem>>>
|
||||
|
||||
@GET("/audio-content/series/main/day-of-week")
|
||||
fun getDayOfWeekSeriesList(
|
||||
@Query("dayOfWeek") dayOfWeek: SeriesPublishedDaysOfWeek,
|
||||
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
|
||||
@Query("contentType") contentType: ContentType,
|
||||
@Query("page") page: Int,
|
||||
@Query("size") size: Int,
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<List<GetSeriesListResponse.SeriesListItem>>>
|
||||
|
||||
@GET("/audio-content/series/main/genre-list")
|
||||
fun getGenreList(
|
||||
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
|
||||
@Query("contentType") contentType: ContentType,
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<List<GetSeriesGenreListResponse>>>
|
||||
|
||||
@GET("/audio-content/series/main/list-by-genre")
|
||||
fun getSeriesListByGenre(
|
||||
@Query("genreId") genreId: Long,
|
||||
@Query("isAdultContentVisible") isAdultContentVisible: Boolean,
|
||||
@Query("contentType") contentType: ContentType,
|
||||
@Query("page") page: Int,
|
||||
@Query("size") size: Int,
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<GetSeriesListResponse>>
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.series.main
|
||||
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.home.SeriesPublishedDaysOfWeek
|
||||
import kr.co.vividnext.sodalive.settings.ContentType
|
||||
|
||||
class SeriesMainRepository(
|
||||
private val api: SeriesMainApi
|
||||
) {
|
||||
fun fetchData(token: String) = api.fetchHome(
|
||||
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
|
||||
contentType = ContentType.entries[SharedPreferenceManager.contentPreference],
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
fun getRecommendSeriesList(token: String) = api.getRecommendSeriesList(
|
||||
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
|
||||
contentType = ContentType.entries[SharedPreferenceManager.contentPreference],
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
fun getDayOfWeekSeriesList(
|
||||
dayOfWeek: SeriesPublishedDaysOfWeek,
|
||||
page: Int,
|
||||
size: Int,
|
||||
token: String
|
||||
) = api.getDayOfWeekSeriesList(
|
||||
dayOfWeek = dayOfWeek,
|
||||
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
|
||||
contentType = ContentType.entries[SharedPreferenceManager.contentPreference],
|
||||
page = page - 1,
|
||||
size = size,
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
fun getGenreList(token: String) = api.getGenreList(
|
||||
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
|
||||
contentType = ContentType.entries[SharedPreferenceManager.contentPreference],
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
fun getSeriesListByGenre(
|
||||
genreId: Long,
|
||||
page: Int,
|
||||
size: Int,
|
||||
token: String
|
||||
) = api.getSeriesListByGenre(
|
||||
genreId = genreId,
|
||||
isAdultContentVisible = SharedPreferenceManager.isAdultContentVisible,
|
||||
contentType = ContentType.entries[SharedPreferenceManager.contentPreference],
|
||||
page = page - 1,
|
||||
size = size,
|
||||
authHeader = token
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.series.main.by_genre
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.databinding.ItemHomeContentThemeBinding
|
||||
|
||||
class GenreAdapter(
|
||||
private val onClickItem: (GetSeriesGenreListResponse) -> Unit
|
||||
) : RecyclerView.Adapter<GenreAdapter.ViewHolder>() {
|
||||
|
||||
private val items = mutableListOf<GetSeriesGenreListResponse>()
|
||||
private var selectedGenreId: Long? = null
|
||||
|
||||
inner class ViewHolder(
|
||||
private val binding: ItemHomeContentThemeBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun bind(item: GetSeriesGenreListResponse) {
|
||||
binding.tvTheme.text = item.genre
|
||||
if (item.id == selectedGenreId) {
|
||||
binding.tvTheme.setBackgroundResource(R.drawable.bg_round_corner_999_3bb9f1)
|
||||
} else {
|
||||
binding.tvTheme.setBackgroundResource(R.drawable.bg_round_corner_999_263238)
|
||||
}
|
||||
binding.root.setOnClickListener {
|
||||
selectedGenreId = item.id
|
||||
onClickItem(item)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
|
||||
ItemHomeContentThemeBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.bind(items[position])
|
||||
}
|
||||
|
||||
override fun getItemCount() = items.size
|
||||
|
||||
fun submitList(list: List<GetSeriesGenreListResponse>, preselectId: Long?) {
|
||||
items.clear()
|
||||
items.addAll(list)
|
||||
selectedGenreId = preselectId
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user