Compare commits

...

38 Commits

Author SHA1 Message Date
klaus 5e44949094 real agora app id 변경 2023-08-09 16:42:29 +09:00
klaus bb1e260a4c 푸시메시지 터치 액션 추가 2023-08-09 13:52:26 +09:00
klaus 68f2896031 모든 기기에서 로그아웃 추가 2023-08-09 08:56:04 +09:00
klaus a75217ee09 고객센터 페이지 추가 2023-08-09 08:39:24 +09:00
klaus e8b4134956 라이브 예약 중 전체 보기 페이지 추가 2023-08-09 08:21:27 +09:00
klaus 035fa5a2fa 라이브 전체 보기 페이지 추가 2023-08-09 08:09:47 +09:00
klaus 8db80f2da9 추천 라이브 터치 액션 추가 2023-08-09 07:55:48 +09:00
klaus 6d93da2136 팔로잉 채널 전체보기 페이지 추가 2023-08-09 07:53:38 +09:00
klaus f455aa81a2 라이브 메인 - 팔로잉 채널 API 연동 2023-08-09 07:27:38 +09:00
klaus 1b53aec571 푸시 - setSmallIcon 추가 2023-08-09 07:22:40 +09:00
klaus 6126525ebb PlaybackTracking DI 등록 2023-08-07 14:25:16 +09:00
klaus 0563fe6890 accountId -> memberId 이름 변경 2023-08-07 14:21:26 +09:00
klaus 1329ae5e5d 콘텐츠 기능 추가 2023-08-05 01:25:09 +09:00
klaus 7dbbd8d490 라이브 예약 현황, 예약 취소 추가 2023-08-02 23:56:45 +09:00
klaus c5896529e1 내 채널 보기 버튼 액션 추가 2023-08-02 17:44:48 +09:00
klaus ce35be9688 메시지 상세 페이지 추가 2023-08-02 17:38:48 +09:00
klaus cc8fab76b0 설정 페이지 추가 2023-08-02 17:26:46 +09:00
klaus 3ef78b64ad 메시지 페이지 추가 2023-08-02 14:57:16 +09:00
klaus 14b652d38e 크리에이터 채널 페이지 추가 2023-08-01 15:10:33 +09:00
klaus 662ef64696 탐색 메인 페이지 추가 2023-08-01 10:29:49 +09:00
klaus c2618669c8 라이브 방 추가 2023-08-01 07:04:16 +09:00
klaus 8a094adc4f 라이브 - 시작, 취소, 입장, 수정, 예약 기능 추가 2023-07-31 17:15:46 +09:00
klaus 0cbf2abf5e 라이브 방 상세 보기 추가 2023-07-30 21:16:16 +09:00
klaus 79127801c6 코인 충전, 코인 내역 2023-07-30 16:20:58 +09:00
klaus 7fb43b3f91 앱 아이콘 변경 2023-07-29 03:12:21 +09:00
klaus 3b235a8495 캔 충전/사용 내역 UI/API 추가 2023-07-29 03:12:09 +09:00
klaus 03de8eba86 마이페이지 메인 - 코인(캔) 아이콘 변경 2023-07-28 18:24:49 +09:00
klaus bf7a7d69a2 본인인증 추가 2023-07-28 17:39:19 +09:00
klaus 8e0a5ccc91 마이페이지 메인 - UI, Api 적용 2023-07-28 15:01:48 +09:00
klaus bad5e6612a 라이브 메인 - UI, Api 적용 2023-07-27 06:46:26 +09:00
klaus 6f86663a54 FCM 설정
FCM 토큰 업데이트 API 적용
2023-07-25 03:20:42 +09:00
klaus fd8c4e726d Firebase 추가
Crashlytics 추가
RemoteConfig 이용한 강제 업데이트 로직 추가
2023-07-25 02:40:41 +09:00
klaus edbaceba0b 회원가입 후 초기 알림설정 기능 추가 2023-07-24 14:54:15 +09:00
klaus 6c8183b12f 이용약관 보기 - 패키지 이동 2023-07-24 06:17:32 +09:00
klaus d60eb7e408 메인 - 하단 탭 추가 2023-07-24 06:15:06 +09:00
klaus 41c6228af5 스플래시 - UI 적용 2023-07-24 05:50:46 +09:00
klaus d562e9199c 회원가입, 로그인 페이지 추가 2023-07-24 05:38:49 +09:00
klaus c1054c5ede KOIN 설정 적용 2023-07-23 19:50:34 +09:00
747 changed files with 48917 additions and 80 deletions

7
.gitignore vendored
View File

@ -20,7 +20,8 @@ bin/
gen/
out/
# Uncomment the following line in case you need and you don't have the release build type files in your app
release/
/release/
/debug/
# Gradle files
.gradle/
@ -302,7 +303,7 @@ fabric.properties
### AndroidStudio Patch ###
!/gradle/wrapper/gradle-wrapper.jar
app/debug/*
app/release/*
app/debug/
app/release/
# End of https://www.toptal.com/developers/gitignore/api/macos,android,androidstudio,visualstudiocode,git,kotlin,java

View File

@ -1,6 +1,7 @@
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'
@ -8,6 +9,7 @@ plugins {
id 'org.jlleitschuh.gradle.ktlint'
id 'io.objectbox'
id("com.google.firebase.crashlytics")
}
android {
@ -18,7 +20,9 @@ android {
enabled true
}
buildFeatures.dataBinding = true
buildFeatures {
dataBinding true
}
lintOptions {
checkDependencies true
@ -43,13 +47,22 @@ android {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
buildConfigField 'String', 'BASE_URL', '"https://api.sodalive.net"'
buildConfigField 'String', 'BOOTPAY_APP_ID', '"64c35be1d25985001dc50c87"'
buildConfigField 'String', 'AGORA_APP_ID', '"e34e40046e9847baba3adfe2b8ffb4f6"'
buildConfigField 'String', 'AGORA_APP_CERTIFICATE', '"15cadeea4ba94ff7b091c9a10f4bf4a6"'
}
debug {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
applicationIdSuffix '.debug'
buildConfigField 'String', 'BASE_URL', '"https://test-api.sodalive.net"'
buildConfigField 'String', 'BOOTPAY_APP_ID', '"6242a7772701800023f68b2e"'
buildConfigField 'String', 'AGORA_APP_ID', '"b96574e191a9430fa54c605528aa3ef7"'
buildConfigField 'String', 'AGORA_APP_CERTIFICATE', '"ae18ade3afcf4086bd4397726eb0654c"'
}
}
compileOptions {
@ -91,7 +104,7 @@ dependencies {
}
// Gson
implementation "com.google.code.gson:gson:2.9.0"
implementation "com.google.code.gson:gson:2.9.1"
// Network
implementation "com.squareup.retrofit2:retrofit:2.9.0"
@ -107,5 +120,32 @@ dependencies {
// permission
implementation "io.github.ParkSangGwon:tedpermission-normal:3.3.0"
implementation 'com.github.dhaval2404:imagepicker:2.1'
implementation 'com.github.zhpanvip:bannerviewpager:3.5.7'
implementation 'com.google.android.gms:play-services-oss-licenses:17.0.1'
// Firebase
implementation platform('com.google.firebase:firebase-bom:32.2.0')
implementation 'com.google.firebase:firebase-dynamic-links-ktx'
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'
// bootpay
implementation "io.github.bootpay:android:4.3.4"
// agora
implementation "io.agora.rtc:voice-sdk:4.1.0-1"
implementation 'io.agora.rtm:rtm-sdk:1.5.3'
// sound visualizer
implementation "com.gauravk.audiovisualizer:audiovisualizer:0.9.2"
// 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"
}

View File

@ -0,0 +1,67 @@
{
"_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
}

View File

@ -0,0 +1,39 @@
{
"project_info": {
"project_number": "758414412471",
"project_id": "sodalive-test",
"storage_bucket": "sodalive-test.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:758414412471:android:dcea9dff87fa125c7a5b32",
"android_client_info": {
"package_name": "kr.co.vividnext.sodalive.debug"
}
},
"oauth_client": [
{
"client_id": "758414412471-g35socquiplhaamhfl4e6bsta5blabi7.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyAeNDVDY_r5afz97L1NPvQC6oFy5lPXHNI"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "758414412471-g35socquiplhaamhfl4e6bsta5blabi7.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
}
],
"configuration_version": "1"
}

View File

@ -2,7 +2,34 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission
android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission
android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30" />
<uses-permission
android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation"
tools:targetApi="s" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="com.google.android.gms.permission.AD_ID" />
<application
android:name=".app.SodaLiveApp"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
@ -10,7 +37,7 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:largeHeap="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:requestLegacyExternalStorage="true"
android:supportsRtl="true"
android:theme="@style/Theme.SodaLive"
android:usesCleartextTraffic="true"
@ -25,6 +52,43 @@
</intent-filter>
</activity>
<activity android:name=".main.MainActivity" />
<activity android:name=".user.login.LoginActivity" />
<activity android:name=".user.signup.SignUpActivity" />
<activity android:name=".settings.terms.TermsActivity" />
<activity android:name=".user.find_password.FindPasswordActivity" />
<activity android:name=".mypage.can.status.CanStatusActivity" />
<activity android:name=".mypage.can.charge.CanChargeActivity" />
<activity android:name=".mypage.can.payment.CanPaymentActivity" />
<activity android:name=".live.room.create.LiveRoomCreateActivity" />
<activity android:name=".live.room.update.LiveRoomEditActivity" />
<activity android:name=".live.reservation.complete.LiveReservationCompleteActivity" />
<activity android:name=".live.room.LiveRoomActivity" />
<activity android:name=".explorer.profile.UserProfileActivity" />
<activity android:name=".explorer.profile.donation.UserProfileDonationAllViewActivity" />
<activity android:name=".explorer.profile.fantalk.UserProfileFantalkAllViewActivity" />
<activity android:name=".explorer.profile.CreatorNoticeWriteActivity" />
<activity android:name=".explorer.profile.follow.UserFollowerListActivity" />
<activity android:name=".message.text.TextMessageWriteActivity" />
<activity android:name=".message.text.TextMessageDetailActivity" />
<activity android:name=".message.SelectMessageRecipientActivity" />
<activity android:name=".settings.SettingsActivity" />
<activity android:name=".settings.signout.SignOutActivity" />
<activity android:name=".settings.notice.NoticeActivity" />
<activity android:name=".settings.notice.NoticeDetailActivity" />
<activity android:name=".settings.event.EventActivity" />
<activity android:name=".settings.event.EventDetailActivity" />
<activity android:name=".settings.notification.NotificationSettingsActivity" />
<activity android:name=".live.reservation_status.LiveReservationStatusActivity" />
<activity android:name=".live.reservation_status.LiveReservationCancelActivity" />
<activity android:name=".audio_content.AudioContentActivity" />
<activity android:name=".audio_content.detail.AudioContentDetailActivity" />
<activity android:name=".audio_content.modify.AudioContentModifyActivity" />
<activity android:name=".audio_content.order.AudioContentOrderListActivity" />
<activity android:name=".audio_content.upload.AudioContentUploadActivity" />
<activity android:name=".following.FollowingCreatorActivity" />
<activity android:name=".live.now.all.LiveNowAllActivity" />
<activity android:name=".live.reservation.all.LiveReservationAllActivity" />
<activity android:name=".mypage.service_center.ServiceCenterActivity" />
<activity
android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity"
@ -32,6 +96,27 @@
<activity
android:name="com.google.android.gms.oss.licenses.OssLicensesActivity"
android:theme="@style/Theme.AppCompat.DayNight" />
</application>
<service
android:name=".common.SodaLiveService"
android:stopWithTask="false" />
<service android:name=".audio_content.AudioContentPlayService" />
<!-- [START firebase_service] -->
<service
android:name=".fcm.SodaFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<!-- [END firebase_service] -->
<!-- [START fcm_default_channel] -->
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="@string/default_notification_channel_id" />
<!-- [END fcm_default_channel] -->
</application>
</manifest>

View File

@ -0,0 +1,205 @@
package kr.co.vividnext.sodalive.agora
import android.content.Context
import com.orhanobut.logger.Logger
import io.agora.rtc2.Constants
import io.agora.rtc2.IRtcEngineEventHandler
import io.agora.rtc2.RtcEngine
import io.agora.rtm.ErrorInfo
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 kr.co.vividnext.sodalive.BuildConfig
import kr.co.vividnext.sodalive.live.room.LiveRoomRequestType
import kotlin.concurrent.thread
class Agora(
private val context: Context,
private val rtcEventHandler: IRtcEngineEventHandler,
private val rtmClientListener: RtmClientListener
) {
// RTM client instance
private var rtmClient: RtmClient? = null
// RTM channel instance
private var rtmChannel: RtmChannel? = null
private var rtcEngine: RtcEngine? = null
init {
initAgoraEngine()
}
private fun initAgoraEngine() {
try {
rtcEngine = RtcEngine.create(
context,
BuildConfig.AGORA_APP_ID,
rtcEventHandler
)
rtcEngine!!.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING)
rtcEngine!!.setAudioProfile(
Constants.AUDIO_PROFILE_MUSIC_HIGH_QUALITY_STEREO,
Constants.AUDIO_SCENARIO_GAME_STREAMING
)
rtcEngine!!.enableAudio()
rtcEngine!!.enableAudioVolumeIndication(500, 3, true)
rtmClient = RtmClient.createInstance(
context,
BuildConfig.AGORA_APP_ID,
rtmClientListener
)
} 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) {
rtcEngine!!.joinChannel(
rtcToken,
channelName,
"",
uid
)
}
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)
}
fun muteLocalAudioStream(muted: Boolean) {
rtcEngine?.muteLocalAudioStream(muted)
}
fun muteAllRemoteAudioStreams(mute: Boolean) {
rtcEngine?.muteAllRemoteAudioStreams(mute)
}
fun sendRawMessageToPeer(
receiverUid: String,
requestType: LiveRoomRequestType,
onSuccess: () -> Unit
) {
val option = SendMessageOptions()
val message = rtmClient!!.createMessage()
message.rawMessage = requestType.toString().toByteArray()
rtmClient!!.sendMessageToPeer(
receiverUid,
message,
option,
object : ResultCallback<Void?> {
override fun onSuccess(aVoid: Void?) {
onSuccess()
}
override fun onFailure(errorInfo: ErrorInfo) {
}
}
)
}
fun rtmChannelIsNull(): Boolean {
return rtmChannel == null
}
fun getConnectionState(): Int {
return rtcEngine!!.connectionState
}
}

View File

@ -0,0 +1,50 @@
package kr.co.vividnext.sodalive.app
import android.app.Application
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.os.Build
import androidx.appcompat.app.AppCompatDelegate
import com.orhanobut.logger.AndroidLogAdapter
import com.orhanobut.logger.Logger
import kr.co.vividnext.sodalive.BuildConfig
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.di.AppDI
class SodaLiveApp : Application() {
override fun onCreate() {
super.onCreate()
Logger.addLogAdapter(object : AndroidLogAdapter() {
override fun isLoggable(priority: Int, tag: String?): Boolean {
return BuildConfig.DEBUG && isDebuggable()
}
})
AppDI(applicationContext, BuildConfig.DEBUG && isDebuggable())
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
SharedPreferenceManager.init(applicationContext)
}
private fun isDebuggable(): Boolean {
var debuggable = false
try {
val appInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
packageManager.getApplicationInfo(
packageName,
PackageManager.ApplicationInfoFlags.of(0L)
)
} else {
packageManager.getApplicationInfo(packageName, 0)
}
debuggable = 0 != appInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE
} catch (e: PackageManager.NameNotFoundException) {
/* debuggable variable will remain false */
}
return debuggable
}
}

View File

@ -0,0 +1,15 @@
package kr.co.vividnext.sodalive.audio_content
import com.google.gson.annotations.SerializedName
import java.util.TimeZone
data class AddAllPlaybackTrackingRequest(
@SerializedName("timezone") val timezone: String = TimeZone.getDefault().id,
@SerializedName("trackingDataList") val trackingDataList: List<PlaybackTrackingData>
)
data class PlaybackTrackingData(
@SerializedName("contentId") val contentId: Long,
@SerializedName("playDateTime") val playDateTime: String,
@SerializedName("isPreview") val isPreview: Boolean,
)

View File

@ -0,0 +1,212 @@
package kr.co.vividnext.sodalive.audio_content
import android.annotation.SuppressLint
import android.app.Activity
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.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadActivity
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.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
class AudioContentActivity : BaseActivity<ActivityAudioContentBinding>(
ActivityAudioContentBinding::inflate
) {
private val viewModel: AudioContentViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var audioContentAdapter: AudioContentAdapter
private var userId: Long = 0
private lateinit var activityResultLauncher: ActivityResultLauncher<Intent>
override fun onCreate(savedInstanceState: Bundle?) {
userId = intent.getLongExtra(Constants.EXTRA_USER_ID, 0)
activityResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode == Activity.RESULT_OK) {
viewModel.page = 1
viewModel.getAudioContentList(userId = userId) { finish() }
}
}
super.onCreate(savedInstanceState)
if (userId <= 0) {
Toast.makeText(applicationContext, "잘못된 요청입니다.", Toast.LENGTH_LONG).show()
finish()
}
bindData()
viewModel.getAudioContentList(userId = userId) { finish() }
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.text = "콘텐츠 전체보기"
binding.toolbar.tvBack.setOnClickListener { finish() }
audioContentAdapter = AudioContentAdapter {
val intent = Intent(applicationContext, AudioContentDetailActivity::class.java)
.apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
}
activityResultLauncher.launch(intent)
}
binding.rvAudioContent.layoutManager = LinearLayoutManager(
applicationContext,
LinearLayoutManager.VERTICAL,
false
)
binding.rvAudioContent.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
audioContentAdapter.itemCount - 1 -> {
outRect.bottom = 0
}
else -> {
outRect.bottom = 13.3f.dpToPx().toInt()
}
}
}
})
binding.rvAudioContent.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.getAudioContentList(userId = userId) { }
}
}
})
binding.rvAudioContent.adapter = audioContentAdapter
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)
}
if (userId == SharedPreferenceManager.userId) {
binding.tvNewContent.visibility = View.VISIBLE
binding.tvNewContent.setOnClickListener {
startActivity(
Intent(
applicationContext,
AudioContentUploadActivity::class.java
)
)
}
} else {
binding.tvNewContent.visibility = View.GONE
}
}
@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.audioContentListLiveData.observe(this) {
if (viewModel.page - 1 == 1) {
audioContentAdapter.items.clear()
binding.rvAudioContent.scrollToPosition(0)
}
binding.tvTotalCount.text = "${it.totalCount}"
audioContentAdapter.items.addAll(it.items)
audioContentAdapter.notifyDataSetChanged()
}
viewModel.sort.observe(this) {
deselectSort()
selectSort(
when (it) {
AudioContentViewModel.Sort.PRICE_HIGH -> {
binding.tvSortPriceHigh
}
AudioContentViewModel.Sort.PRICE_LOW -> {
binding.tvSortPriceLow
}
else -> {
binding.tvSortNewest
}
}
)
viewModel.getAudioContentList(userId = userId) { finish() }
}
}
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
)
)
}
}

View File

@ -0,0 +1,73 @@
package kr.co.vividnext.sodalive.audio_content
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.RoundedCornersTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemAudioContentBinding
import kr.co.vividnext.sodalive.explorer.profile.GetAudioContentListItem
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.moneyFormat
class AudioContentAdapter(
private val onClickItem: (Long) -> Unit
) : RecyclerView.Adapter<AudioContentAdapter.ViewHolder>() {
val items = mutableListOf<GetAudioContentListItem>()
inner class ViewHolder(
private val binding: ItemAudioContentBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetAudioContentListItem) {
binding.ivCover.load(item.coverImageUrl) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(RoundedCornersTransformation(5.3f.dpToPx()))
}
binding.tvTitle.text = item.title
binding.tvTheme.text = item.themeStr
binding.tvDuration.text = item.duration
binding.tvLikeCount.text = item.likeCount.moneyFormat()
binding.tvCommentCount.text = item.commentCount.moneyFormat()
if (item.price < 1) {
binding.tvPrice.text = "무료"
binding.tvPrice.setCompoundDrawables(null, null, null, null)
} else {
binding.tvPrice.text = item.price.moneyFormat()
binding.tvPrice.setCompoundDrawablesWithIntrinsicBounds(
R.drawable.ic_coin_w,
0,
0,
0
)
}
binding.iv19.visibility = if (item.isAdult) {
View.VISIBLE
} else {
View.GONE
}
binding.root.setOnClickListener { onClickItem(item.contentId) }
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
ItemAudioContentBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
}
override fun getItemCount() = items.count()
}

View File

@ -0,0 +1,140 @@
package kr.co.vividnext.sodalive.audio_content
import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.audio_content.comment.GetAudioContentCommentListResponse
import kr.co.vividnext.sodalive.audio_content.comment.RegisterAudioContentCommentRequest
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
import kr.co.vividnext.sodalive.audio_content.donation.AudioContentDonationRequest
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainResponse
import kr.co.vividnext.sodalive.audio_content.order.GetAudioContentOrderListResponse
import kr.co.vividnext.sodalive.audio_content.order.OrderRequest
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 okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.http.Body
import retrofit2.http.DELETE
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Part
import retrofit2.http.Path
import retrofit2.http.Query
interface AudioContentApi {
@GET("/audio-content")
fun getAudioContentList(
@Query("creator-id") id: Long,
@Query("page") page: Int,
@Query("size") size: Int,
@Query("sort-type") sort: AudioContentViewModel.Sort,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetAudioContentListResponse>>
@GET("/audio-content/theme")
fun getAudioContentThemeList(
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentThemeResponse>>>
@POST("/audio-content")
@Multipart
fun uploadAudioContent(
@Part coverImage: MultipartBody.Part,
@Part contentFile: MultipartBody.Part,
@Part("request") request: RequestBody,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@GET("/audio-content/{id}")
fun getAudioContentDetail(
@Path("id") id: Long,
@Query("timezone") timezone: String,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetAudioContentDetailResponse>>
@POST("/order/audio-content")
fun orderAudioContent(
@Body request: OrderRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@GET("/order/audio-content")
fun getAudioContentOrderList(
@Query("page") page: Int,
@Query("size") size: Int,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetAudioContentOrderListResponse>>
@POST("/audio-content/playback-tracking")
fun addAllPlaybackTracking(
@Body request: AddAllPlaybackTrackingRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@POST("/audio-content/comment")
fun registerComment(
@Body request: RegisterAudioContentCommentRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@GET("/audio-content/{id}/comment")
fun getAudioContentCommentList(
@Path("id") id: Long,
@Query("page") page: Int,
@Query("size") size: Int,
@Query("timezone") timezone: String,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetAudioContentCommentListResponse>>
@GET("/audio-content/comment/{id}")
fun getAudioContentCommentCommentList(
@Path("id") id: Long,
@Query("page") page: Int,
@Query("size") size: Int,
@Query("timezone") timezone: String,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetAudioContentCommentListResponse>>
@PUT("/audio-content/like")
fun likeContent(
@Body request: PutAudioContentLikeRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<PutAudioContentLikeResponse>>
@PUT("/audio-content")
@Multipart
fun modifyAudioContent(
@Part coverImage: MultipartBody.Part?,
@Part("request") request: RequestBody,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@DELETE("/audio-content/{id}")
fun deleteAudioContent(
@Path("id") id: Long,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@GET("/audio-content/main")
fun getMain(
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetAudioContentMainResponse>>
@GET("/audio-content/main/new")
fun getNewContentOfTheme(
@Query("theme") theme: String,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetAudioContentMainItem>>>
@POST("/audio-content/donation")
fun donation(
@Body request: AudioContentDonationRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
}

View File

@ -0,0 +1,556 @@
package kr.co.vividnext.sodalive.audio_content
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.media.AudioAttributes
import android.media.MediaPlayer
import android.os.Build
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import androidx.core.app.NotificationCompat
import com.bumptech.glide.Glide
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.common.Constants
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.main.MainActivity
import org.koin.android.ext.android.inject
class AudioContentPlayService :
Service(),
MediaPlayer.OnPreparedListener,
MediaPlayer.OnCompletionListener {
private var playbackTrackingId: Long = 0
private val playbackTrackingRepository: PlaybackTrackingRepository by inject()
private lateinit var mediaPlayer: MediaPlayer
private var url: String? = null
private var title: String? = null
private var isFree: Boolean? = null
private var isPreview: Boolean? = null
private var nickname: String? = null
private var contentId: Long? = null
private var creatorId: Long? = null
private var coverImageUrl: String? = null
private var isPlaying = false
private val handler = Handler(Looper.getMainLooper())
private var changeMediaPlayerPositionRunnable = object : Runnable {
override fun run() {
val intent = Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_PROGRESS, mediaPlayer.currentPosition)
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, contentId)
}
sendBroadcast(intent)
handler.postDelayed(this, 1000)
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
MusicAction.INIT.name -> {
val contentId = intent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0)
if (this.contentId != null && this.contentId == contentId) {
sendBroadcast(
Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER)
.apply {
putExtra(
Constants.EXTRA_AUDIO_CONTENT_CHANGE_UI,
true
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_PLAYING,
isPlaying
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_DURATION,
mediaPlayer.duration
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_PROGRESS,
mediaPlayer.currentPosition
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_ID,
contentId
)
}
)
}
if (
this.contentId != null &&
title != null &&
nickname != null &&
coverImageUrl != null
) {
sendBroadcast(
Intent(Constants.ACTION_MAIN_AUDIO_CONTENT_RECEIVER)
.apply {
putExtra(
Constants.EXTRA_AUDIO_CONTENT_SHOWING,
true
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_PLAYING,
isPlaying
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_TITLE,
title
)
putExtra(
Constants.EXTRA_NICKNAME,
nickname
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_COVER_IMAGE_URL,
coverImageUrl
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_ID,
this@AudioContentPlayService.contentId
)
}
)
} else {
sendBroadcast(
Intent(Constants.ACTION_MAIN_AUDIO_CONTENT_RECEIVER)
.apply {
putExtra(
Constants.EXTRA_AUDIO_CONTENT_SHOWING,
false
)
}
)
}
}
MusicAction.PLAY.name -> {
if (!isPlaying) {
mediaPlayer.start()
toggleIsPlaying()
updateNotification()
}
}
MusicAction.PAUSE.name -> {
if (isPlaying) {
mediaPlayer.pause()
toggleIsPlaying()
updateNotification()
}
}
MusicAction.STOP.name -> {
if (this::mediaPlayer.isInitialized) {
mediaPlayer.stop()
setEndPositionPlaybackTracking(mediaPlayer.currentPosition)
toggleIsPlaying(false)
onStopService()
}
}
MusicAction.CONDITIONAL_STOP.name -> {
val contentId = intent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0)
if (
this.contentId != null &&
this.contentId == contentId &&
this::mediaPlayer.isInitialized
) {
mediaPlayer.stop()
setEndPositionPlaybackTracking(mediaPlayer.currentPosition)
toggleIsPlaying(false)
onStopService()
}
}
MusicAction.PROGRESS.name -> {
val progress = intent.getIntExtra(Constants.EXTRA_AUDIO_CONTENT_PROGRESS, 0)
if (progress > 0) {
if (contentId != null) saveNewPlaybackTracking(
totalDuration = mediaPlayer.duration,
progress = progress
)
mediaPlayer.seekTo(progress)
}
}
else -> {
val contentId = intent?.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0)
if (contentId != null && this.contentId == contentId) {
if (isPlaying) {
sendBroadcast(
Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER)
.apply {
putExtra(
Constants.EXTRA_AUDIO_CONTENT_NEXT_ACTION,
MusicAction.PAUSE
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_ID,
contentId
)
}
)
} else {
sendBroadcast(
Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER)
.apply {
putExtra(
Constants.EXTRA_AUDIO_CONTENT_NEXT_ACTION,
MusicAction.PLAY
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_ID,
contentId
)
}
)
}
} else {
url = intent?.getStringExtra(Constants.EXTRA_AUDIO_CONTENT_URL)
title = intent?.getStringExtra(Constants.EXTRA_AUDIO_CONTENT_TITLE)
isFree = intent?.getBooleanExtra(
Constants.EXTRA_AUDIO_CONTENT_FREE,
true
)
isPreview = intent?.getBooleanExtra(
Constants.EXTRA_AUDIO_CONTENT_PREVIEW,
true
)
nickname = intent?.getStringExtra(Constants.EXTRA_NICKNAME)
coverImageUrl = intent?.getStringExtra(
Constants.EXTRA_AUDIO_CONTENT_COVER_IMAGE_URL
)
creatorId = intent?.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_CREATOR_ID, 0)
this.contentId = contentId
if (url != null) {
sendBroadcast(
Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER)
.apply {
putExtra(
Constants.EXTRA_AUDIO_CONTENT_LOADING,
true
)
}
)
if (isPlaying) {
mediaPlayer.stop()
setEndPositionPlaybackTracking(mediaPlayer.currentPosition)
mediaPlayer.release()
toggleIsPlaying()
}
initMediaPlayer()
mediaPlayer.setDataSource(url)
mediaPlayer.prepareAsync()
}
}
}
}
return START_NOT_STICKY
}
override fun onDestroy() {
if (this::mediaPlayer.isInitialized) {
mediaPlayer.release()
}
onStopService()
super.onDestroy()
}
override fun onBind(p0: Intent?): IBinder? {
return null
}
override fun onCompletion(mp: MediaPlayer?) {
setEndPositionPlaybackTracking(mediaPlayer.currentPosition)
if (SharedPreferenceManager.isContentPlayLoop) {
saveNewPlaybackTracking(totalDuration = mediaPlayer.duration, progress = 0)
mediaPlayer.start()
} else {
toggleIsPlaying(false)
mediaPlayer.release()
onStopService()
}
}
private fun toggleIsPlaying(isPlaying: Boolean? = null) {
this.isPlaying = isPlaying ?: !this.isPlaying
if (this.isPlaying) {
handler.postDelayed(changeMediaPlayerPositionRunnable, 1000)
} else {
handler.removeCallbacks(changeMediaPlayerPositionRunnable)
}
sendBroadcast(
Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER)
.apply {
putExtra(
Constants.EXTRA_AUDIO_CONTENT_CHANGE_UI,
true
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_PLAYING,
this@AudioContentPlayService.isPlaying
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_ID,
contentId
)
}
)
if (isPlaying != null && !isPlaying) {
resetAudioData()
}
sendBroadcast(
Intent(Constants.ACTION_MAIN_AUDIO_CONTENT_RECEIVER)
.apply {
putExtra(
Constants.EXTRA_AUDIO_CONTENT_PLAYING,
this@AudioContentPlayService.isPlaying
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_SHOWING,
contentId != null
)
}
)
}
private fun resetAudioData() {
url = null
title = null
nickname = null
contentId = null
}
private fun initMediaPlayer() {
mediaPlayer = MediaPlayer()
mediaPlayer.setOnPreparedListener(this)
mediaPlayer.setOnCompletionListener(this)
mediaPlayer.setAudioAttributes(
AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build()
)
}
override fun onPrepared(mp: MediaPlayer?) {
saveNewPlaybackTracking(totalDuration = mediaPlayer.duration, progress = 0)
sendBroadcast(
Intent(Constants.ACTION_AUDIO_CONTENT_RECEIVER)
.apply {
putExtra(
Constants.EXTRA_AUDIO_CONTENT_NEXT_ACTION,
MusicAction.PLAY
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_DURATION,
mediaPlayer.duration
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_ID,
contentId
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_ALERT_PREVIEW,
true
)
}
)
sendBroadcast(
Intent(Constants.ACTION_MAIN_AUDIO_CONTENT_RECEIVER)
.apply {
putExtra(
Constants.EXTRA_AUDIO_CONTENT_PLAYING,
false
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_SHOWING,
true
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_TITLE,
title
)
putExtra(
Constants.EXTRA_NICKNAME,
nickname
)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_COVER_IMAGE_URL,
coverImageUrl
)
}
)
}
private fun updateNotification() {
val intent = Intent(this, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
val pendingIntent = PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
)
val channelId = "audio_content_play_channel"
val notificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
channelId,
"콘텐츠 알림 채널",
NotificationManager.IMPORTANCE_LOW
)
notificationManager.createNotificationChannel(channel)
}
val playPauseIcon =
if (isPlaying) R.drawable.ic_noti_pause else R.drawable.ic_noti_play
val playPauseAction =
if (isPlaying) MusicAction.PAUSE.name else MusicAction.PLAY.name
Glide
.with(this)
.asBitmap()
.load(coverImageUrl)
.into(object : CustomTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
val notificationBuilder = NotificationCompat
.Builder(this@AudioContentPlayService, channelId)
.setSmallIcon(R.drawable.ic_noti)
.setLargeIcon(resource)
.setContentTitle(title ?: "오디오 콘텐츠")
.setContentText(nickname ?: "")
.setContentIntent(pendingIntent)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.addAction(
NotificationCompat
.Action
.Builder(
playPauseIcon,
"Play_or_Pause",
getServiceIntent(playPauseAction)
).build()
)
.addAction(
NotificationCompat
.Action
.Builder(
R.drawable.ic_noti_stop,
"Stop",
getServiceIntent(MusicAction.STOP.name)
).build()
)
notificationBuilder.setStyle(
androidx.media.app.NotificationCompat.MediaStyle()
.setShowActionsInCompactView(0, 1)
)
startForeground(1, notificationBuilder.build())
}
override fun onLoadCleared(placeholder: Drawable?) {
}
})
}
private fun getServiceIntent(action: String): PendingIntent {
val intent = Intent(this, AudioContentPlayService::class.java)
intent.action = action
return PendingIntent.getService(
this,
0,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
)
}
private fun saveNewPlaybackTracking(totalDuration: Int, progress: Int) {
if (creatorId != SharedPreferenceManager.userId) {
playbackTrackingId = playbackTrackingRepository.savePlaybackTracking(
PlaybackTracking(
contentId = contentId!!,
totalDuration = totalDuration,
startPosition = progress,
isFree = isFree!!,
isPreview = isPreview!!
)
)
}
}
private fun setEndPositionPlaybackTracking(progress: Int) {
if (creatorId != SharedPreferenceManager.userId && playbackTrackingId > 0) {
val playbackTracking = playbackTrackingRepository
.getPlaybackTracking(playbackTrackingId)
if (playbackTracking != null) {
playbackTracking.endPosition = progress
playbackTrackingRepository.savePlaybackTracking(playbackTracking)
}
playbackTrackingId = 0
}
}
private fun onStopService() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
stopForeground(STOP_FOREGROUND_REMOVE)
} else {
stopForeground(true)
}
stopSelf()
}
enum class MusicAction {
PLAY, PAUSE, STOP, PROGRESS, INIT, CONDITIONAL_STOP
}
}

View File

@ -0,0 +1,138 @@
package kr.co.vividnext.sodalive.audio_content
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 okhttp3.MultipartBody
import okhttp3.RequestBody
import java.util.TimeZone
class AudioContentRepository(
private val api: AudioContentApi,
private val userApi: UserApi
) {
fun getAudioContentList(
id: Long,
page: Int,
size: Int,
sort: AudioContentViewModel.Sort,
token: String
) = api.getAudioContentList(
id = id,
page = page - 1,
size = size,
sort = sort,
authHeader = token
)
fun getAudioContentThemeList(token: String) = api.getAudioContentThemeList(token)
fun uploadAudioContent(
coverImage: MultipartBody.Part,
contentFile: MultipartBody.Part,
request: RequestBody,
token: String
) = api.uploadAudioContent(
coverImage = coverImage,
contentFile = contentFile,
request = request,
authHeader = token
)
fun modifyAudioContent(
coverImage: MultipartBody.Part? = null,
request: RequestBody,
token: String
) = api.modifyAudioContent(
coverImage = coverImage,
request = request,
authHeader = token
)
fun deleteAudioContent(
id: Long,
token: String
) = api.deleteAudioContent(
id = id,
authHeader = token
)
fun getAudioContentDetail(audioContentId: Long, token: String) = api.getAudioContentDetail(
id = audioContentId,
timezone = TimeZone.getDefault().id,
authHeader = token
)
fun registerNotification(
creatorId: Long,
token: String
) = userApi.creatorFollow(
request = CreatorFollowRequestRequest(creatorId = creatorId),
authHeader = token
)
fun unRegisterNotification(
creatorId: Long,
token: String
) = userApi.creatorUnFollow(
request = CreatorFollowRequestRequest(creatorId = creatorId),
authHeader = token
)
fun orderContent(
contentId: Long,
orderType: OrderType,
token: String
) = api.orderAudioContent(
request = OrderRequest(
contentId = contentId,
orderType = orderType,
container = "aos"
),
authHeader = token
)
fun getAudioContentOrderList(
page: Int,
size: Int,
token: String
) = api.getAudioContentOrderList(
page = page - 1,
size = size,
authHeader = token
)
fun addAllPlaybackTracking(
request: AddAllPlaybackTrackingRequest,
token: String
) = api.addAllPlaybackTracking(request, authHeader = token)
fun likeContent(
request: PutAudioContentLikeRequest,
token: String
) = api.likeContent(request, authHeader = token)
fun getMain(token: String) = api.getMain(authHeader = token)
fun getNewContentOfTheme(theme: String, token: String) = api.getNewContentOfTheme(
theme = theme,
authHeader = token
)
fun donation(
contentId: Long,
can: Int,
comment: String,
token: String
) = api.donation(
request = AudioContentDonationRequest(
contentId = contentId,
donationCan = can,
comment = comment
),
authHeader = token
)
}

View File

@ -0,0 +1,102 @@
package kr.co.vividnext.sodalive.audio_content
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.google.gson.annotations.SerializedName
import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.explorer.profile.GetAudioContentListResponse
class AudioContentViewModel(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 _audioContentListLiveData = MutableLiveData<GetAudioContentListResponse>()
val audioContentListLiveData: LiveData<GetAudioContentListResponse>
get() = _audioContentListLiveData
private val _sort = MutableLiveData(Sort.NEWEST)
val sort: LiveData<Sort>
get() = _sort
enum class Sort {
@SerializedName("NEWEST")
NEWEST,
@SerializedName("PRICE_HIGH")
PRICE_HIGH,
@SerializedName("PRICE_LOW")
PRICE_LOW
}
private var isLast = false
var page = 1
private val size = 10
fun getAudioContentList(userId: Long, onFailure: (() -> Unit)? = null) {
if (!_isLoading.value!! && !isLast) {
_isLoading.value = true
compositeDisposable.add(
repository.getAudioContentList(
id = userId,
page = page,
size = size,
token = "Bearer ${SharedPreferenceManager.token}",
sort = _sort.value!!
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
if (it.data.items.isNotEmpty()) {
page += 1
_audioContentListLiveData.postValue(it.data!!)
} else {
isLast = true
}
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
if (onFailure != null) {
onFailure()
}
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
if (onFailure != null) {
onFailure()
}
}
)
)
}
}
fun changeSort(sort: Sort) {
page = 1
isLast = false
_sort.postValue(sort)
}
}

View File

@ -0,0 +1,23 @@
package kr.co.vividnext.sodalive.audio_content
import io.objectbox.annotation.Entity
import io.objectbox.annotation.Id
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@Entity
data class PlaybackTracking(
@Id
var id: Long = 0,
var contentId: Long,
var totalDuration: Int,
var startPosition: Int,
var isFree: Boolean,
var isPreview: Boolean,
var endPosition: Int? = null,
var playDateTime: String = SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss",
Locale.getDefault()
).format(Date())
)

View File

@ -0,0 +1,29 @@
package kr.co.vividnext.sodalive.audio_content
import kr.co.vividnext.sodalive.common.ObjectBox
class PlaybackTrackingRepository(private val objectBox: ObjectBox) {
fun savePlaybackTracking(data: PlaybackTracking): Long {
return objectBox.playbackTrackingBox.put(data)
}
fun getPlaybackTracking(id: Long): PlaybackTracking? {
val query = objectBox.playbackTrackingBox
.query(PlaybackTracking_.id.equal(id))
.build()
val playbackTracking = query.findFirst()
query.close()
return playbackTracking
}
fun getAllPlaybackTracking(): List<PlaybackTracking> {
return objectBox
.playbackTrackingBox
.all
}
fun removeAllPlaybackTracking() {
objectBox.playbackTrackingBox.removeAll()
}
}

View File

@ -0,0 +1,103 @@
package kr.co.vividnext.sodalive.audio_content.comment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemAudioContentCommentBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.moneyFormat
class AudioContentCommentAdapter(
private val onItemClick: (GetAudioContentCommentListItem) -> Unit
) : RecyclerView.Adapter<AudioContentCommentAdapter.ViewHolder>() {
var items = mutableSetOf<GetAudioContentCommentListItem>()
inner class ViewHolder(
private val binding: ItemAudioContentCommentBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetAudioContentCommentListItem) {
binding.ivCommentProfile.load(item.profileUrl) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(CircleCropTransformation())
}
val tvCommentLayoutParams = binding.tvComment.layoutParams as LinearLayout.LayoutParams
val coin = item.donationCoin
if (coin > 0) {
tvCommentLayoutParams.topMargin = 0
binding.llDonationCoin.visibility = View.VISIBLE
binding.tvDonationCoin.text = coin.moneyFormat()
binding.llDonationCoin.setBackgroundResource(
when {
coin >= 100000 -> {
R.drawable.bg_round_corner_10_7_973a3a
}
coin >= 50000 -> {
R.drawable.bg_round_corner_10_7_d85e37
}
coin >= 10000 -> {
R.drawable.bg_round_corner_10_7_d38c38
}
coin >= 5000 -> {
R.drawable.bg_round_corner_10_7_59548f
}
coin >= 1000 -> {
R.drawable.bg_round_corner_10_7_4d6aa4
}
coin >= 500 -> {
R.drawable.bg_round_corner_10_7_2d7390
}
else -> {
R.drawable.bg_round_corner_10_7_548f7d
}
}
)
} else {
tvCommentLayoutParams.topMargin = 13.3f.dpToPx().toInt()
binding.llDonationCoin.visibility = View.GONE
}
binding.tvComment.layoutParams = tvCommentLayoutParams
binding.tvComment.text = item.comment
binding.tvCommentDate.text = item.date
binding.tvCommentNickname.text = item.nickname
binding.tvWriteReply.text = if (item.replyCount > 0) {
"답글 ${item.replyCount}"
} else {
"답글 쓰기"
}
binding.tvWriteReply.setOnClickListener { onItemClick(item) }
binding.root.setOnClickListener { onItemClick(item) }
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
ItemAudioContentCommentBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items.toList()[position])
}
override fun getItemCount() = items.size
}

View File

@ -0,0 +1,72 @@
package kr.co.vividnext.sodalive.audio_content.comment
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
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.databinding.DialogAudioContentCommentBinding
class AudioContentCommentFragment(private val audioContentId: Long) : BottomSheetDialogFragment() {
private lateinit var binding: DialogAudioContentCommentBinding
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState)
dialog.setOnShowListener {
val d = it 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
}
}
return dialog
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = DialogAudioContentCommentBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val commentListFragmentTag = "COMMENT_LIST_FRAGMENT"
val commentListFragment = AudioContentCommentListFragment.newInstance(
audioContentId = audioContentId
)
val fragmentTransaction = childFragmentManager.beginTransaction()
fragmentTransaction.add(R.id.fl_container, commentListFragment, commentListFragmentTag)
fragmentTransaction.addToBackStack(commentListFragmentTag)
fragmentTransaction.commit()
}
fun hideCommentDialog() {
dialog?.dismiss()
}
fun onClickComment(comment: GetAudioContentCommentListItem) {
val commentReplyFragmentTag = "COMMENT_REPLY_FRAGMENT"
val commentReplyFragment = AudioContentCommentReplyFragment.newInstance(
audioContentId = audioContentId,
comment = comment
)
val fragmentTransaction = childFragmentManager.beginTransaction()
fragmentTransaction.add(R.id.fl_container, commentReplyFragment, commentReplyFragmentTag)
fragmentTransaction.addToBackStack(commentReplyFragmentTag)
fragmentTransaction.commit()
}
}

View File

@ -0,0 +1,182 @@
package kr.co.vividnext.sodalive.audio_content.comment
import android.annotation.SuppressLint
import android.app.Service
import android.graphics.Rect
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.FragmentAudioContentCommentListBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
class AudioContentCommentListFragment : BaseFragment<FragmentAudioContentCommentListBinding>(
FragmentAudioContentCommentListBinding::inflate
) {
private val viewModel: AudioContentCommentListViewModel by inject()
private lateinit var imm: InputMethodManager
private lateinit var loadingDialog: LoadingDialog
private lateinit var adapter: AudioContentCommentAdapter
private var audioContentId: Long = 0
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
audioContentId = arguments?.getLong(Constants.EXTRA_AUDIO_CONTENT_ID) ?: 0
return super.onCreateView(inflater, container, savedInstanceState)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
imm = requireContext().getSystemService(
Service.INPUT_METHOD_SERVICE
) as InputMethodManager
setupView()
bindData()
viewModel.getCommentList(audioContentId = audioContentId) { hideDialog() }
}
private fun hideDialog() {
(parentFragment as AudioContentCommentFragment).hideCommentDialog()
}
private fun setupView() {
binding.ivClose.setOnClickListener { hideDialog() }
binding.ivCommentProfile.load(SharedPreferenceManager.profileImage) {
crossfade(true)
placeholder(R.drawable.ic_logo)
transformations(CircleCropTransformation())
}
binding.ivCommentSend.setOnClickListener {
hideKeyboard()
val comment = binding.etComment.text.toString()
binding.etComment.setText("")
viewModel.registerComment(audioContentId, comment)
}
adapter = AudioContentCommentAdapter {
(parentFragment as AudioContentCommentFragment).onClickComment(it)
}
val recyclerView = binding.rvComment
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 = 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.getCommentList(audioContentId = audioContentId)
}
}
})
recyclerView.adapter = adapter
}
@SuppressLint("NotifyDataSetChanged")
private fun bindData() {
viewModel.isLoading.observe(viewLifecycleOwner) {
if (it) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
}
}
viewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
}
viewModel.totalCommentCount.observe(viewLifecycleOwner) {
binding.tvCommentCount.text = "$it"
}
viewModel.commentList.observe(viewLifecycleOwner) {
if (viewModel.page - 1 == 1) {
adapter.items.clear()
binding.rvComment.scrollToPosition(0)
}
adapter.items.addAll(it)
adapter.notifyDataSetChanged()
}
}
private fun hideKeyboard() {
imm.hideSoftInputFromWindow(view?.windowToken, 0)
}
companion object {
fun newInstance(audioContentId: Long): AudioContentCommentListFragment {
val args = Bundle()
args.putLong(Constants.EXTRA_AUDIO_CONTENT_ID, audioContentId)
val fragment = AudioContentCommentListFragment()
fragment.arguments = args
return fragment
}
}
}

View File

@ -0,0 +1,125 @@
package kr.co.vividnext.sodalive.audio_content.comment
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.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
class AudioContentCommentListViewModel(
private val repository: AudioContentCommentRepository
) : 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 _commentList = MutableLiveData<List<GetAudioContentCommentListItem>>()
val commentList: LiveData<List<GetAudioContentCommentListItem>>
get() = _commentList
private var _totalCommentCount = MutableLiveData(0)
val totalCommentCount: LiveData<Int>
get() = _totalCommentCount
var page = 1
private var isLast = false
private val size = 10
fun getCommentList(audioContentId: Long, onFailure: (() -> Unit)? = null) {
if (!_isLoading.value!! && !isLast) {
_isLoading.value = true
compositeDisposable.add(
repository.getAudioContentCommentList(
audioContentId = audioContentId,
page = page,
size = size,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
_totalCommentCount.postValue(it.data.totalCount)
if (it.data.items.isNotEmpty()) {
page += 1
_commentList.postValue(it.data.items)
} else {
isLast = true
}
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
if (onFailure != null) {
onFailure()
}
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
if (onFailure != null) {
onFailure()
}
}
)
)
}
}
fun registerComment(contentId: Long, comment: String) {
if (!_isLoading.value!!) {
_isLoading.value = true
}
compositeDisposable.add(
repository.registerComment(
contentId = contentId,
comment = comment,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success) {
page = 1
isLast = false
getCommentList(contentId)
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
}

View File

@ -0,0 +1,93 @@
package kr.co.vividnext.sodalive.audio_content.comment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import coil.load
import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemAudioContentCommentBinding
import kr.co.vividnext.sodalive.databinding.ItemAudioContentCommentReplyBinding
class AudioContentCommentReplyAdapter :
RecyclerView.Adapter<AudioContentCommentReplyViewHolder>() {
var items = mutableSetOf<GetAudioContentCommentListItem>()
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): AudioContentCommentReplyViewHolder {
return if (viewType == 0) {
AudioContentCommentReplyHeaderViewHolder(
ItemAudioContentCommentBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
} else {
AudioContentCommentReplyItemViewHolder(
ItemAudioContentCommentReplyBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
}
override fun onBindViewHolder(holder: AudioContentCommentReplyViewHolder, position: Int) {
holder.bind(items.toList()[position])
}
override fun getItemCount() = items.size
override fun getItemViewType(position: Int): Int {
return position
}
}
abstract class AudioContentCommentReplyViewHolder(
binding: ViewBinding
) : RecyclerView.ViewHolder(binding.root) {
abstract fun bind(item: GetAudioContentCommentListItem)
}
class AudioContentCommentReplyHeaderViewHolder(
private val binding: ItemAudioContentCommentBinding
) : AudioContentCommentReplyViewHolder(binding) {
override fun bind(item: GetAudioContentCommentListItem) {
binding.ivCommentProfile.load(item.profileUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
transformations(CircleCropTransformation())
}
binding.tvComment.text = item.comment
binding.tvCommentDate.text = item.date
binding.tvCommentNickname.text = item.nickname
binding.tvWriteReply.visibility = View.GONE
}
}
class AudioContentCommentReplyItemViewHolder(
private val binding: ItemAudioContentCommentReplyBinding
) : AudioContentCommentReplyViewHolder(binding) {
override fun bind(item: GetAudioContentCommentListItem) {
binding.ivCommentProfile.load(item.profileUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
transformations(CircleCropTransformation())
}
binding.tvComment.text = item.comment
binding.tvCommentDate.text = item.date
binding.tvCommentNickname.text = item.nickname
}
}

View File

@ -0,0 +1,204 @@
package kr.co.vividnext.sodalive.audio_content.comment
import android.annotation.SuppressLint
import android.app.Service
import android.graphics.Rect
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.core.os.BundleCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.FragmentAudioContentCommentReplyBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
class AudioContentCommentReplyFragment : BaseFragment<FragmentAudioContentCommentReplyBinding>(
FragmentAudioContentCommentReplyBinding::inflate
) {
private val viewModel: AudioContentCommentReplyViewModel by inject()
private lateinit var imm: InputMethodManager
private lateinit var loadingDialog: LoadingDialog
private lateinit var adapter: AudioContentCommentReplyAdapter
private var originalComment: GetAudioContentCommentListItem? = null
private var audioContentId: Long = 0
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
audioContentId = arguments?.getLong(Constants.EXTRA_AUDIO_CONTENT_ID) ?: 0
originalComment = BundleCompat.getParcelable(
requireArguments(),
Constants.EXTRA_AUDIO_CONTENT_COMMENT,
GetAudioContentCommentListItem::class.java
)
return super.onCreateView(inflater, container, savedInstanceState)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (originalComment == null) {
parentFragmentManager.popBackStack()
}
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
imm = requireContext().getSystemService(
Service.INPUT_METHOD_SERVICE
) as InputMethodManager
setupView()
bindData()
viewModel.getCommentReplyList(commentId = originalComment!!.id) {
parentFragmentManager.popBackStack()
}
}
private fun hideDialog() {
(parentFragment as AudioContentCommentFragment).hideCommentDialog()
}
private fun setupView() {
binding.root.setOnClickListener { }
binding.tvBack.setOnClickListener {
parentFragmentManager.popBackStack()
}
binding.ivClose.setOnClickListener { hideDialog() }
binding.ivCommentProfile.load(SharedPreferenceManager.profileImage) {
crossfade(true)
placeholder(R.drawable.ic_logo)
transformations(CircleCropTransformation())
}
binding.ivCommentSend.setOnClickListener {
hideKeyboard()
val comment = binding.etComment.text.toString()
binding.etComment.setText("")
viewModel.registerComment(audioContentId, originalComment!!.id, comment)
}
adapter = AudioContentCommentReplyAdapter().apply {
items.add(originalComment!!)
}
val recyclerView = binding.rvCommentReply
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 = 13.3f.dpToPx().toInt()
outRect.right = 13.3f.dpToPx().toInt()
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.top = 13.3f.dpToPx().toInt()
outRect.bottom = 12f.dpToPx().toInt()
}
adapter.itemCount - 1 -> {
outRect.top = 12f.dpToPx().toInt()
outRect.bottom = 13.3f.dpToPx().toInt()
}
else -> {
outRect.top = 12f.dpToPx().toInt()
outRect.bottom = 12f.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.getCommentReplyList(originalComment!!.id)
}
}
})
recyclerView.adapter = adapter
}
@SuppressLint("NotifyDataSetChanged")
private fun bindData() {
viewModel.isLoading.observe(viewLifecycleOwner) {
if (it) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
}
}
viewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
}
viewModel.commentList.observe(viewLifecycleOwner) {
if (viewModel.page - 1 == 1) {
adapter.items.clear()
binding.rvCommentReply.scrollToPosition(0)
adapter.items.add(originalComment!!)
}
adapter.items.addAll(it)
adapter.notifyDataSetChanged()
}
}
private fun hideKeyboard() {
imm.hideSoftInputFromWindow(view?.windowToken, 0)
}
companion object {
fun newInstance(
audioContentId: Long,
comment: GetAudioContentCommentListItem
): AudioContentCommentReplyFragment {
val args = Bundle()
args.putLong(Constants.EXTRA_AUDIO_CONTENT_ID, audioContentId)
args.putParcelable(Constants.EXTRA_AUDIO_CONTENT_COMMENT, comment)
val fragment = AudioContentCommentReplyFragment()
fragment.arguments = args
return fragment
}
}
}

View File

@ -0,0 +1,120 @@
package kr.co.vividnext.sodalive.audio_content.comment
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.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
class AudioContentCommentReplyViewModel(
private val repository: AudioContentCommentRepository
) : 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 _commentList = MutableLiveData<List<GetAudioContentCommentListItem>>()
val commentList: LiveData<List<GetAudioContentCommentListItem>>
get() = _commentList
var page = 1
private var isLast = false
private val size = 10
fun getCommentReplyList(commentId: Long, onFailure: (() -> Unit)? = null) {
if (!_isLoading.value!! && !isLast) {
_isLoading.value = true
compositeDisposable.add(
repository.getAudioContentCommentReplyList(
commentId = commentId,
page = page,
size = size,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
if (it.data.items.isNotEmpty()) {
page += 1
_commentList.postValue(it.data.items)
} else {
isLast = true
}
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
if (onFailure != null) {
onFailure()
}
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
if (onFailure != null) {
onFailure()
}
}
)
)
}
}
fun registerComment(contentId: Long, commentId: Long, comment: String) {
if (!_isLoading.value!!) {
_isLoading.value = true
}
compositeDisposable.add(
repository.registerComment(
contentId = contentId,
comment = comment,
parentId = commentId,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success) {
page = 1
isLast = false
getCommentReplyList(commentId)
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
}

View File

@ -0,0 +1,46 @@
package kr.co.vividnext.sodalive.audio_content.comment
import kr.co.vividnext.sodalive.audio_content.AudioContentApi
import java.util.TimeZone
class AudioContentCommentRepository(private val api: AudioContentApi) {
fun registerComment(
contentId: Long,
comment: String,
parentId: Long? = null,
token: String
) = api.registerComment(
request = RegisterAudioContentCommentRequest(
comment = comment,
contentId = contentId,
parentId = parentId
),
authHeader = token
)
fun getAudioContentCommentList(
audioContentId: Long,
page: Int,
size: Int,
token: String
) = api.getAudioContentCommentList(
id = audioContentId,
page = page - 1,
size = size,
timezone = TimeZone.getDefault().id,
authHeader = token
)
fun getAudioContentCommentReplyList(
commentId: Long,
page: Int,
size: Int,
token: String
) = api.getAudioContentCommentCommentList(
id = commentId,
page = page - 1,
size = size,
timezone = TimeZone.getDefault().id,
authHeader = token
)
}

View File

@ -0,0 +1,21 @@
package kr.co.vividnext.sodalive.audio_content.comment
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
data class GetAudioContentCommentListResponse(
@SerializedName("totalCount") val totalCount: Int,
@SerializedName("items") val items: List<GetAudioContentCommentListItem>
)
@Parcelize
data class GetAudioContentCommentListItem(
@SerializedName("id") val id: Long,
@SerializedName("nickname") val nickname: String,
@SerializedName("profileUrl") val profileUrl: String,
@SerializedName("comment") val comment: String,
@SerializedName("donationCoin") val donationCoin: Int,
@SerializedName("date") val date: String,
@SerializedName("replyCount") val replyCount: Int
) : Parcelable

View File

@ -0,0 +1,9 @@
package kr.co.vividnext.sodalive.audio_content.comment
import com.google.gson.annotations.SerializedName
data class RegisterAudioContentCommentRequest(
@SerializedName("comment") val comment: String,
@SerializedName("contentId") val contentId: Long,
@SerializedName("parentId") val parentId: Long?
)

View File

@ -0,0 +1,67 @@
package kr.co.vividnext.sodalive.audio_content.detail
import android.annotation.SuppressLint
import android.app.Activity
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.LayoutInflater
import android.view.WindowManager
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import kr.co.vividnext.sodalive.databinding.DialogAudioContentDeleteBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
@SuppressLint("SetTextI18n")
class AudioContentDeleteDialog(
activity: Activity,
layoutInflater: LayoutInflater,
title: String,
confirmButtonClick: () -> Unit
) {
private val alertDialog: AlertDialog
val dialogView = DialogAudioContentDeleteBinding.inflate(layoutInflater)
init {
val dialogBuilder = AlertDialog.Builder(activity)
dialogBuilder.setView(dialogView.root)
alertDialog = dialogBuilder.create()
alertDialog.setCancelable(false)
alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
dialogView.tvTitle.text = "[$title]을 삭제하시겠습니까?"
dialogView.tvCancel.setOnClickListener {
alertDialog.dismiss()
}
dialogView.tvConfirm.setOnClickListener {
if (dialogView.tvNotice.isSelected) {
alertDialog.dismiss()
confirmButtonClick()
} else {
Toast.makeText(
activity,
"동의하셔야 삭제할 수 있습니다.",
Toast.LENGTH_LONG
).show()
}
}
dialogView.tvNotice.setOnClickListener {
it.isSelected = !it.isSelected
}
}
fun show(width: Int) {
alertDialog.show()
val lp = WindowManager.LayoutParams()
lp.copyFrom(alertDialog.window?.attributes)
lp.width = width - (26.7f.dpToPx()).toInt()
lp.height = WindowManager.LayoutParams.WRAP_CONTENT
alertDialog.window?.attributes = lp
}
}

View File

@ -0,0 +1,788 @@
package kr.co.vividnext.sodalive.audio_content.detail
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.graphics.Rect
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.widget.RelativeLayout
import android.widget.SeekBar
import android.widget.Toast
import androidx.appcompat.widget.PopupMenu
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
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.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.base.BaseActivity
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.ActivityAudioContentDetailBinding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationDialog
import kr.co.vividnext.sodalive.mypage.auth.Auth
import kr.co.vividnext.sodalive.mypage.auth.AuthVerifyRequest
import kr.co.vividnext.sodalive.mypage.auth.BootpayResponse
import kr.co.vividnext.sodalive.report.ReportType
import org.koin.android.ext.android.inject
class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBinding>(
ActivityAudioContentDetailBinding::inflate
) {
private val viewModel: AudioContentDetailViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var creatorOtherContentAdapter: OtherContentAdapter
private lateinit var sameThemeOtherContentAdapter: OtherContentAdapter
private var audioContentId: Long = 0
private var isAlertPreview = false
private val audioContentReceiver = AudioContentReceiver()
private var refresh = false
set(value) {
field = value
setResult(RESULT_OK)
}
private var title = ""
@SuppressLint("SetTextI18n")
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
binding.scrollView.scrollTo(0, 0)
binding.sbProgress.progress = 0
binding.ivPlayOrPause.setImageResource(R.drawable.btn_audio_content_play)
binding.tvTotalDuration.text = " / 00:00:00"
binding.tvCurrentDuration.text = "00:00:00"
binding.rlPreviewAlert.visibility = View.GONE
audioContentId = intent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0)
viewModel.getAudioContentDetail(audioContentId = audioContentId) { finish() }
}
override fun onCreate(savedInstanceState: Bundle?) {
audioContentId = intent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0)
super.onCreate(savedInstanceState)
if (audioContentId <= 0) {
Toast.makeText(applicationContext, "잘못된 요청입니다.", Toast.LENGTH_LONG).show()
finish()
}
bindData()
viewModel.getAudioContentDetail(audioContentId = audioContentId) { finish() }
}
override fun onResume() {
super.onResume()
val intentFilter = IntentFilter(Constants.ACTION_AUDIO_CONTENT_RECEIVER)
registerReceiver(audioContentReceiver, intentFilter)
if (refresh) {
viewModel.getAudioContentDetail(audioContentId = audioContentId) { finish() }
}
}
override fun onPause() {
super.onPause()
unregisterReceiver(audioContentReceiver)
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.tvBack.text = "콘텐츠 상세"
binding.tvBack.setOnClickListener { finish() }
binding.ivClosePreviewAlert.setOnClickListener { viewModel.toggleShowPreviewAlert() }
binding.ivMenu.setOnClickListener {
showOptionMenu(
this,
binding.ivMenu,
)
}
creatorOtherContentAdapter = OtherContentAdapter {
val intent = Intent(applicationContext, AudioContentDetailActivity::class.java)
.apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
}
startActivity(intent)
}
binding.rvCreatorOtherContent.layoutManager = LinearLayoutManager(
applicationContext,
LinearLayoutManager.HORIZONTAL,
false
)
binding.swipeRefreshLayout.setOnRefreshListener {
viewModel.getAudioContentDetail(
audioContentId = audioContentId
) { finish() }
binding.swipeRefreshLayout.isRefreshing = false
}
binding.rvCreatorOtherContent.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()
}
creatorOtherContentAdapter.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.rvCreatorOtherContent.adapter = creatorOtherContentAdapter
sameThemeOtherContentAdapter = OtherContentAdapter {
val intent = Intent(applicationContext, AudioContentDetailActivity::class.java)
.apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
}
startActivity(intent)
}
binding.rvThemeOtherContent.layoutManager = LinearLayoutManager(
applicationContext,
LinearLayoutManager.HORIZONTAL,
false
)
binding.rvThemeOtherContent.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()
}
creatorOtherContentAdapter.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.rvThemeOtherContent.adapter = sameThemeOtherContentAdapter
binding.sbProgress.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
}
override fun onStartTrackingTouch(p0: SeekBar?) {
}
override fun onStopTrackingTouch(seekBar: SeekBar?) {
if (seekBar != null) {
val intent = Intent(
this@AudioContentDetailActivity,
AudioContentPlayService::class.java
)
intent.action = AudioContentPlayService.MusicAction.PROGRESS.name
intent.putExtra(Constants.EXTRA_AUDIO_CONTENT_PROGRESS, seekBar.progress)
startService(intent)
}
}
})
val layoutParams = binding.ivCover.layoutParams as RelativeLayout.LayoutParams
layoutParams.width = (screenWidth - 13.3f.dpToPx()).toInt()
layoutParams.height = (screenWidth - 13.3f.dpToPx()).toInt()
binding.ivCover.layoutParams = layoutParams
binding.ivPlayLoop.setOnClickListener { viewModel.togglePlayLoop() }
binding.llDonation.setOnClickListener {
val dialog = LiveRoomDonationDialog(
this,
LayoutInflater.from(this)
) { can, message ->
if (can <= 0) {
showToast("1코인 이상 후원하실 수 있습니다.")
} else if (message.isBlank()) {
showToast("함께 보낼 메시지를 입력하세요.")
} else {
donation(can, message)
}
}
dialog.show(screenWidth)
}
}
private fun donation(coin: Int, message: String) {
viewModel.donation(audioContentId, coin, message) {
viewModel.getAudioContentDetail(audioContentId = audioContentId) { finish() }
}
}
private fun showOptionMenu(context: Context, v: View) {
val popup = PopupMenu(context, v)
val inflater = popup.menuInflater
if (
viewModel.audioContentLiveData.value!!.creator.creatorId ==
SharedPreferenceManager.userId
) {
inflater.inflate(R.menu.audio_content_detail_creator_menu, popup.menu)
popup.setOnMenuItemClickListener {
when (it.itemId) {
R.id.menu_modify -> {
refresh = true
startActivity(
Intent(applicationContext, AudioContentModifyActivity::class.java)
.apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, audioContentId)
}
)
}
R.id.menu_delete -> {
showDeleteDialog()
}
}
true
}
} else {
inflater.inflate(R.menu.audio_content_detail_user_menu, popup.menu)
popup.setOnMenuItemClickListener {
when (it.itemId) {
R.id.menu_report -> {
showReportDialog()
}
}
true
}
}
popup.show()
}
private fun showDeleteDialog() {
AudioContentDeleteDialog(
this,
layoutInflater,
this.title,
confirmButtonClick = {
viewModel.deleteAudioContent(audioContentId) {
setResult(RESULT_OK)
finish()
}
}
).show(screenWidth)
}
private fun showReportDialog() {
AudioContentReportDialog(this, layoutInflater) {
viewModel.report(
type = ReportType.AUDIO_CONTENT,
contentId = audioContentId,
reason = it
)
}.show(screenWidth)
}
@SuppressLint("NotifyDataSetChanged", "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.isExpandDetail.observe(this) {
binding.tvDetail.maxLines = if (it) {
Int.MAX_VALUE
} else {
2
}
}
viewModel.isShowPreviewAlert.observe(this) {
binding.rlPreviewAlert.visibility = if (it) {
View.VISIBLE
} else {
View.GONE
}
}
viewModel.audioContentLiveData.observe(this) {
refresh = false
startService(
Intent(this, AudioContentPlayService::class.java).apply {
action = AudioContentPlayService.MusicAction.INIT.name
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it.contentId)
}
)
title = it.title
setupCreatorArea(it.creator)
setupMosaicArea(it.isMosaic)
setupPlayArea(it)
setupInfoArea(it)
setupPurchaseButton(it)
setupCommentArea(it)
setupCreatorOtherContentListArea(it.creatorOtherContentList)
setupSameThemeOtherContentList(it.sameThemeOtherContentList)
isAlertPreview = it.creator.creatorId != SharedPreferenceManager.userId &&
!it.existOrdered &&
it.price > 0
}
viewModel.isContentPlayLoopLiveData.observe(this) {
if (it) {
binding.ivPlayLoop.setImageResource(R.drawable.btn_player_repeat)
} else {
binding.ivPlayLoop.setImageResource(R.drawable.btn_player_repeat_done)
}
}
}
@SuppressLint("NotifyDataSetChanged")
private fun setupSameThemeOtherContentList(
sameThemeOtherContentList: List<OtherContentResponse>
) {
if (sameThemeOtherContentList.isEmpty()) {
binding.rvThemeOtherContent.visibility = View.GONE
binding.llThemeOtherContentPreparing.visibility = View.VISIBLE
} else {
binding.rvThemeOtherContent.visibility = View.VISIBLE
binding.llThemeOtherContentPreparing.visibility = View.GONE
sameThemeOtherContentAdapter.items.clear()
sameThemeOtherContentAdapter.items.addAll(sameThemeOtherContentList)
sameThemeOtherContentAdapter.notifyDataSetChanged()
}
}
@SuppressLint("NotifyDataSetChanged")
private fun setupCreatorOtherContentListArea(
creatorOtherContentList: List<OtherContentResponse>
) {
if (creatorOtherContentList.isEmpty()) {
binding.rvCreatorOtherContent.visibility = View.GONE
binding.llCreatorOtherContentPreparing.visibility = View.VISIBLE
} else {
binding.rvCreatorOtherContent.visibility = View.VISIBLE
binding.llCreatorOtherContentPreparing.visibility = View.GONE
creatorOtherContentAdapter.items.clear()
creatorOtherContentAdapter.items.addAll(creatorOtherContentList)
creatorOtherContentAdapter.notifyDataSetChanged()
}
}
private fun setupCommentArea(response: GetAudioContentDetailResponse) {
if (response.isCommentAvailable) {
binding.llDonation.visibility = View.VISIBLE
binding.llComment.visibility = View.VISIBLE
binding.tvCommentCount.text = "${response.commentCount}"
if (response.commentCount > 0) {
binding.ivCommentProfile.load(response.commentList[0].profileUrl) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(CircleCropTransformation())
}
binding.tvCommentText.text = response.commentList[0].comment
binding.tvCommentText.visibility = View.VISIBLE
binding.rlInputComment.visibility = View.GONE
binding.llComment.setOnClickListener { showCommentBottomSheetDialog() }
} else {
binding.tvCommentText.visibility = View.GONE
binding.rlInputComment.visibility = View.VISIBLE
binding.ivCommentProfile.load(SharedPreferenceManager.profileImage) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(CircleCropTransformation())
}
binding.ivCommentSend.setOnClickListener {
val comment = binding.etComment.text.toString()
binding.etComment.setText("")
viewModel.registerComment(audioContentId, comment)
}
binding.llComment.setOnClickListener {}
}
} else {
binding.llComment.visibility = View.GONE
binding.llDonation.visibility = View.GONE
}
}
private fun showCommentBottomSheetDialog() {
val dialog = AudioContentCommentFragment(audioContentId = audioContentId)
dialog.show(
supportFragmentManager,
dialog.tag
)
}
private fun setupPurchaseButton(response: GetAudioContentDetailResponse) {
if (
response.price > 0 &&
!response.existOrdered &&
response.orderType == null &&
response.creator.creatorId != SharedPreferenceManager.userId
) {
binding.llPurchase.visibility = View.VISIBLE
binding.tvPrice.text = response.price.toString()
binding.llPurchase.setOnClickListener {
showOrderDialog(audioContent = response)
}
} else {
binding.llPurchase.visibility = View.GONE
}
}
@SuppressLint("SetTextI18n")
private fun setupPlayArea(response: GetAudioContentDetailResponse) {
Glide
.with(this)
.load(response.coverImageUrl)
.centerCrop()
.placeholder(R.drawable.bg_black)
.apply(RequestOptions().override((screenWidth - 13.3f.dpToPx()).toInt()))
.into(binding.ivCover)
binding.ivPlayOrPause.setOnClickListener {
startService(
Intent(this, AudioContentPlayService::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_COVER_IMAGE_URL, response.coverImageUrl)
putExtra(Constants.EXTRA_AUDIO_CONTENT_URL, response.contentUrl)
putExtra(Constants.EXTRA_NICKNAME, response.creator.nickname)
putExtra(Constants.EXTRA_AUDIO_CONTENT_TITLE, response.title)
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, response.contentId)
putExtra(Constants.EXTRA_AUDIO_CONTENT_CREATOR_ID, response.creator.creatorId)
putExtra(Constants.EXTRA_AUDIO_CONTENT_FREE, response.price <= 0)
putExtra(
Constants.EXTRA_AUDIO_CONTENT_PREVIEW,
!response.existOrdered && response.price > 0
)
}
)
}
binding.tvTotalDuration.text = " / ${response.duration}"
}
@SuppressLint("SetTextI18n")
private fun setupInfoArea(response: GetAudioContentDetailResponse) {
binding.tvTheme.text = response.themeStr
binding.tv19.visibility = if (response.isAdult) {
View.VISIBLE
} else {
View.GONE
}
if (response.orderType != null && response.orderType == OrderType.KEEP) {
binding.tvPurchased.visibility = View.VISIBLE
binding.tvRental.visibility = View.GONE
binding.tvRemainingTime.visibility = View.GONE
} else if (response.orderType != null && response.orderType == OrderType.RENTAL) {
binding.tvPurchased.visibility = View.GONE
binding.tvRental.visibility = View.VISIBLE
binding.tvRemainingTime.visibility = View.VISIBLE
binding.tvRemainingTime.text = response.remainingTime
} else {
binding.tvPurchased.visibility = View.GONE
binding.tvRental.visibility = View.GONE
binding.tvRemainingTime.visibility = View.GONE
}
binding.tvTitle.text = response.title
binding.tvDetail.text = response.detail
binding.tvDetail.setOnClickListener { viewModel.toggleExpandDetail() }
if (response.tag.isNotBlank()) {
binding.tvTag.visibility = View.VISIBLE
binding.tvTag.text = response.tag
} else {
binding.tvTag.visibility = View.GONE
}
binding.ivLike.setImageResource(
if (response.isLike) {
R.drawable.ic_audio_content_heart_pressed
} else {
R.drawable.ic_audio_content_heart_normal
}
)
binding.tvLike.text = "${response.likeCount}"
binding.llLike.setOnClickListener {
viewModel.likeContent(contentId = audioContentId) {
val likeCount = binding.tvLike.text.toString().toInt()
if (it) {
binding.tvLike.text = "${likeCount + 1}"
binding.ivLike.setImageResource(R.drawable.ic_audio_content_heart_pressed)
} else {
binding.tvLike.text = if (likeCount - 1 < 0) {
"0"
} else {
"${likeCount - 1}"
}
binding.ivLike.setImageResource(R.drawable.ic_audio_content_heart_normal)
}
}
}
binding.tvShare.setOnClickListener {
viewModel.shareAudioContent(
audioContentId = audioContentId,
contentImage = response.coverImageUrl,
contentTitle = "${response.title} - ${response.creator.nickname}"
) {
val intent = Intent(Intent.ACTION_SEND)
intent.type = "text/plain"
intent.putExtra(Intent.EXTRA_TEXT, it)
val shareIntent = Intent.createChooser(intent, "오디오콘텐츠 공유")
startActivity(shareIntent)
}
}
}
private fun setupMosaicArea(isMosaic: Boolean) {
if (isMosaic) {
binding.alert19Bg.visibility = View.VISIBLE
binding.llAlert19.visibility = View.VISIBLE
binding.tvAuth.setOnClickListener {
Auth.auth(this, applicationContext) { data ->
val bootpayResponse = Gson().fromJson(data, BootpayResponse::class.java)
val request = AuthVerifyRequest(receiptId = bootpayResponse.data.receiptId)
runOnUiThread {
viewModel.authVerify(audioContentId = audioContentId, request = request)
}
}
}
} else {
binding.alert19Bg.visibility = View.GONE
binding.llAlert19.visibility = View.GONE
binding.tvAuth.setOnClickListener {}
}
}
private fun setupCreatorArea(creator: AudioContentCreator) {
binding.rlProfile.setOnClickListener {
startActivity(
Intent(applicationContext, UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, creator.creatorId)
}
)
}
binding.ivProfile.load(creator.profileImageUrl) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(CircleCropTransformation())
}
binding.tvProfileNickname.text = creator.nickname
if (creator.creatorId != SharedPreferenceManager.userId) {
binding.ivFollow.visibility = View.VISIBLE
if (creator.isFollowing) {
binding.ivFollow.setImageResource(R.drawable.btn_notification_selected)
binding.ivFollow.setOnClickListener {
viewModel.unRegisterNotification(
contentId = audioContentId,
creatorId = creator.creatorId
)
}
} else {
binding.ivFollow.setImageResource(R.drawable.btn_notification)
binding.ivFollow.setOnClickListener {
viewModel.registerNotification(
contentId = audioContentId,
creatorId = creator.creatorId
)
}
}
} else {
binding.ivFollow.visibility = View.GONE
}
}
private fun showOrderDialog(audioContent: GetAudioContentDetailResponse) {
val dialog = AudioContentOrderFragment(
price = audioContent.price,
onClickKeep = { showOrderConfirmDialog(audioContent, OrderType.KEEP) },
onClickRental = { showOrderConfirmDialog(audioContent, OrderType.RENTAL) }
)
dialog.show(
supportFragmentManager,
dialog.tag
)
}
private fun showOrderConfirmDialog(
audioContent: GetAudioContentDetailResponse,
orderType: OrderType
) {
AudioContentOrderConfirmDialog(
activity = this,
layoutInflater = layoutInflater,
title = audioContent.title,
theme = audioContent.themeStr,
coverImageUrl = audioContent.coverImageUrl,
isAdult = audioContent.isAdult,
profileImageUrl = audioContent.creator.profileImageUrl,
nickname = audioContent.creator.nickname,
duration = audioContent.duration,
orderType = orderType,
price = audioContent.price,
confirmButtonClick = {
startService(
Intent(this, AudioContentPlayService::class.java).apply {
action = AudioContentPlayService.MusicAction.CONDITIONAL_STOP.name
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, audioContent.contentId)
}
)
binding.rlPreviewAlert.visibility = View.GONE
viewModel.order(
contentId = audioContent.contentId,
orderType = orderType
)
},
).show(screenWidth)
}
inner class AudioContentReceiver : BroadcastReceiver() {
@SuppressLint("SetTextI18n")
override fun onReceive(context: Context?, intent: Intent?) {
val nextAction = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent?.getSerializableExtra(
Constants.EXTRA_AUDIO_CONTENT_NEXT_ACTION,
AudioContentPlayService.MusicAction::class.java
)
} else {
intent?.getSerializableExtra(Constants.EXTRA_AUDIO_CONTENT_NEXT_ACTION)
}
val duration = intent?.getIntExtra(Constants.EXTRA_AUDIO_CONTENT_DURATION, 0)
val progress = intent?.getIntExtra(Constants.EXTRA_AUDIO_CONTENT_PROGRESS, 0)
val contentId = intent?.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0)
val isPlaying = intent?.getBooleanExtra(Constants.EXTRA_AUDIO_CONTENT_PLAYING, false)
val changeUi = intent?.getBooleanExtra(Constants.EXTRA_AUDIO_CONTENT_CHANGE_UI, false)
val alertPreview = intent?.getBooleanExtra(
Constants.EXTRA_AUDIO_CONTENT_ALERT_PREVIEW,
false
)
val isLoading = intent?.getBooleanExtra(
Constants.EXTRA_AUDIO_CONTENT_LOADING,
false
)
viewModel.isLoading.value = isLoading ?: false
if (this@AudioContentDetailActivity.audioContentId == contentId) {
runOnUiThread {
if (changeUi != null && changeUi) {
binding.ivPlayOrPause.setImageResource(
if (isPlaying != null && isPlaying) {
R.drawable.btn_audio_content_pause
} else {
R.drawable.btn_audio_content_play
}
)
}
}
if (duration != null && duration > 0) {
binding.sbProgress.max = duration
binding.tvTotalDuration.text = " / ${Utils.convertDurationToString(duration)}"
}
if (progress != null && progress > 0) {
binding.sbProgress.progress = progress
binding.tvCurrentDuration.text = Utils.convertDurationToString(progress)
}
if (alertPreview != null && alertPreview && isAlertPreview) {
viewModel.toggleShowPreviewAlert()
}
if (nextAction != null) {
startService(
Intent(
this@AudioContentDetailActivity,
AudioContentPlayService::class.java
).apply {
action = (nextAction as AudioContentPlayService.MusicAction).name
}
)
}
}
}
}
}

View File

@ -0,0 +1,475 @@
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
import kr.co.vividnext.sodalive.audio_content.AudioContentRepository
import kr.co.vividnext.sodalive.audio_content.comment.AudioContentCommentRepository
import kr.co.vividnext.sodalive.audio_content.order.OrderType
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.extensions.moneyFormat
import kr.co.vividnext.sodalive.mypage.auth.AuthRepository
import kr.co.vividnext.sodalive.mypage.auth.AuthVerifyRequest
import kr.co.vividnext.sodalive.report.ReportRepository
import kr.co.vividnext.sodalive.report.ReportRequest
import kr.co.vividnext.sodalive.report.ReportType
class AudioContentDetailViewModel(
private val repository: AudioContentRepository,
private val authRepository: AuthRepository,
private val reportRepository: ReportRepository,
private val commentRepository: AudioContentCommentRepository
) : BaseViewModel() {
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
var isLoading = MutableLiveData(false)
private set
private var _audioContentLiveData = MutableLiveData<GetAudioContentDetailResponse>()
val audioContentLiveData: LiveData<GetAudioContentDetailResponse>
get() = _audioContentLiveData
private val _isExpandDetail = MutableLiveData(false)
val isExpandDetail: LiveData<Boolean>
get() = _isExpandDetail
private val _isShowPreviewAlert = MutableLiveData(false)
val isShowPreviewAlert: LiveData<Boolean>
get() = _isShowPreviewAlert
private val _isContentPlayLoopLiveData = MutableLiveData(
SharedPreferenceManager.isContentPlayLoop
)
val isContentPlayLoopLiveData: LiveData<Boolean>
get() = _isContentPlayLoopLiveData
fun getAudioContentDetail(audioContentId: Long, onFailure: (() -> Unit)? = null) {
if (!isLoading.value!!) {
isLoading.value = true
}
compositeDisposable.add(
repository.getAudioContentDetail(
audioContentId = audioContentId,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
_audioContentLiveData.postValue(it.data!!)
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
if (onFailure != null) {
onFailure()
}
}
isLoading.value = false
},
{
isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
if (onFailure != null) {
onFailure()
}
}
)
)
}
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!!
}
fun toggleShowPreviewAlert() {
_isShowPreviewAlert.value = !_isShowPreviewAlert.value!!
}
fun order(contentId: Long, orderType: OrderType) {
isLoading.value = true
compositeDisposable.add(
repository.orderContent(
contentId = contentId,
orderType = orderType,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
getAudioContentDetail(audioContentId = contentId)
_toastLiveData.postValue("구매가 완료되었습니다.")
} 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 authVerify(audioContentId: Long, request: AuthVerifyRequest) {
if (!isLoading.value!!) {
isLoading.value = true
}
compositeDisposable.add(
authRepository.verify(request, "Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success) {
getAudioContentDetail(audioContentId)
} 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 registerComment(audioContentId: Long, comment: String) {
if (!isLoading.value!!) {
isLoading.value = true
}
compositeDisposable.add(
commentRepository.registerComment(
contentId = audioContentId,
comment = comment,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success) {
getAudioContentDetail(audioContentId)
} 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 likeContent(contentId: Long, onSuccess: (Boolean) -> Unit) {
if (!isLoading.value!!) {
isLoading.value = true
}
compositeDisposable.add(
repository.likeContent(
request = PutAudioContentLikeRequest(contentId = contentId),
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
isLoading.value = false
if (it.success) {
if (it.data != null) {
onSuccess(it.data.like)
} else {
getAudioContentDetail(contentId)
}
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
},
{
isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
fun shareAudioContent(
audioContentId: Long,
contentImage: String,
contentTitle: String,
onSuccess: (String) -> Unit
) {
isLoading.value = true
Firebase.dynamicLinks.shortLinkAsync(ShortDynamicLink.Suffix.SHORT) {
link = Uri.parse("https://yozm.day/?audio_content_id=$audioContentId")
domainUriPrefix = "https://yozm.page.link"
androidParameters { }
iosParameters("kr.co.vividnext.yozm") {
appStoreId = "1630284226"
}
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
}
}
fun deleteAudioContent(audioContentId: Long, onSuccess: () -> Unit) {
isLoading.value = true
compositeDisposable.add(
repository.deleteAudioContent(
audioContentId,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
isLoading.value = false
if (it.success) {
_toastLiveData.postValue(
"삭제되었습니다."
)
onSuccess()
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
},
{
isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
fun report(type: ReportType, contentId: Long, reason: String) {
isLoading.value = true
val request = ReportRequest(type = type, reason = reason, contentId = contentId)
compositeDisposable.add(
reportRepository.report(
request = request,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
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 togglePlayLoop() {
val isPlayLoop = !SharedPreferenceManager.isContentPlayLoop
SharedPreferenceManager.isContentPlayLoop = isPlayLoop
_isContentPlayLoopLiveData.value = isPlayLoop
}
fun donation(contentId: Long, can: Int, comment: String, onSuccess: () -> Unit) {
isLoading.value = true
compositeDisposable.add(
repository.donation(
contentId = contentId,
can = can,
comment = comment,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
isLoading.value = false
if (it.success) {
SharedPreferenceManager.can -= can
_toastLiveData.postValue(
"${can.moneyFormat()}캔을 후원하였습니다."
)
onSuccess()
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
},
{
isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
}

View File

@ -0,0 +1,60 @@
package kr.co.vividnext.sodalive.audio_content.detail
import android.app.Activity
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.LayoutInflater
import android.view.WindowManager
import android.widget.RadioButton
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import kr.co.vividnext.sodalive.databinding.DialogAudioContentReportBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
class AudioContentReportDialog(
activity: Activity,
layoutInflater: LayoutInflater,
confirmButtonClick: (String) -> Unit
) {
private val alertDialog: AlertDialog
val dialogView = DialogAudioContentReportBinding.inflate(layoutInflater)
var reason = ""
init {
val dialogBuilder = AlertDialog.Builder(activity)
dialogBuilder.setView(dialogView.root)
alertDialog = dialogBuilder.create()
alertDialog.setCancelable(false)
alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
dialogView.tvCancel.setOnClickListener {
alertDialog.dismiss()
}
dialogView.tvReport.setOnClickListener {
if (reason.isNotBlank()) {
alertDialog.dismiss()
confirmButtonClick(reason)
} else {
Toast.makeText(activity, "신고 이유를 선택하세요.", Toast.LENGTH_LONG).show()
}
}
dialogView.radioGroup.setOnCheckedChangeListener { radioGroup, checkedId ->
val radioButton = radioGroup.findViewById<RadioButton>(checkedId)
reason = radioButton.text.toString()
}
}
fun show(width: Int) {
alertDialog.show()
val lp = WindowManager.LayoutParams()
lp.copyFrom(alertDialog.window?.attributes)
lp.width = width - (26.7f.dpToPx()).toInt()
lp.height = WindowManager.LayoutParams.WRAP_CONTENT
alertDialog.window?.attributes = lp
}
}

View File

@ -0,0 +1,45 @@
package kr.co.vividnext.sodalive.audio_content.detail
import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.audio_content.comment.GetAudioContentCommentListItem
import kr.co.vividnext.sodalive.audio_content.order.OrderType
data class GetAudioContentDetailResponse(
@SerializedName("contentId") val contentId: Long,
@SerializedName("title") val title: String,
@SerializedName("detail") val detail: String,
@SerializedName("coverImageUrl") val coverImageUrl: String,
@SerializedName("contentUrl") val contentUrl: String,
@SerializedName("themeStr") val themeStr: String,
@SerializedName("tag") val tag: String,
@SerializedName("price") val price: Int,
@SerializedName("duration") val duration: String,
@SerializedName("isAdult") val isAdult: Boolean,
@SerializedName("isMosaic") val isMosaic: Boolean,
@SerializedName("existOrdered") val existOrdered: Boolean,
@SerializedName("orderType") val orderType: OrderType?,
@SerializedName("remainingTime") val remainingTime: String?,
@SerializedName("creatorOtherContentList")
val creatorOtherContentList: List<OtherContentResponse>,
@SerializedName("sameThemeOtherContentList")
val sameThemeOtherContentList: List<OtherContentResponse>,
@SerializedName("isCommentAvailable") val isCommentAvailable: Boolean,
@SerializedName("isLike") val isLike: Boolean,
@SerializedName("likeCount") val likeCount: Int,
@SerializedName("commentList") val commentList: List<GetAudioContentCommentListItem>,
@SerializedName("commentCount") val commentCount: Int,
@SerializedName("creator") val creator: AudioContentCreator
)
data class OtherContentResponse(
@SerializedName("contentId") val contentId: Long,
@SerializedName("title") val title: String,
@SerializedName("coverUrl") val coverUrl: String,
)
data class AudioContentCreator(
@SerializedName("creatorId") val creatorId: Long,
@SerializedName("nickname") val nickname: String,
@SerializedName("profileImageUrl") val profileImageUrl: String,
@SerializedName("isFollowing") val isFollowing: Boolean
)

View File

@ -0,0 +1,46 @@
package kr.co.vividnext.sodalive.audio_content.detail
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.ItemAudioOtherContentBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
class OtherContentAdapter(
private val onClickItem: (Long) -> Unit
) : RecyclerView.Adapter<OtherContentAdapter.ViewHolder>() {
val items = mutableListOf<OtherContentResponse>()
inner class ViewHolder(
private val binding: ItemAudioOtherContentBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: OtherContentResponse) {
binding.ivCover.load(item.coverUrl) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(RoundedCornersTransformation(2.7f.dpToPx()))
}
binding.tvTitle.text = item.title
binding.root.setOnClickListener { onClickItem(item.contentId) }
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
ItemAudioOtherContentBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
}
override fun getItemCount() = items.count()
}

View File

@ -0,0 +1,11 @@
package kr.co.vividnext.sodalive.audio_content.detail
import com.google.gson.annotations.SerializedName
data class PutAudioContentLikeRequest(
@SerializedName("contentId") val contentId: Long
)
data class PutAudioContentLikeResponse(
@SerializedName("like") val like: Boolean
)

View File

@ -0,0 +1,10 @@
package kr.co.vividnext.sodalive.audio_content.donation
import com.google.gson.annotations.SerializedName
data class AudioContentDonationRequest(
@SerializedName("contentId") val contentId: Long,
@SerializedName("donationCan") val donationCan: Int,
@SerializedName("comment") val comment: String,
@SerializedName("container") val container: String = "aos"
)

View File

@ -0,0 +1,41 @@
package kr.co.vividnext.sodalive.audio_content.main
import android.widget.FrameLayout
import android.widget.ImageView
import coil.load
import coil.transform.RoundedCornersTransformation
import com.zhpan.bannerview.BaseBannerAdapter
import com.zhpan.bannerview.BaseViewHolder
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.extensions.dpToPx
class AudioContentMainBannerAdapter(
private val itemWidth: Int,
private val itemHeight: Int,
private val onClick: (GetAudioContentBannerResponse) -> Unit
) : BaseBannerAdapter<GetAudioContentBannerResponse>() {
override fun bindData(
holder: BaseViewHolder<GetAudioContentBannerResponse>,
data: GetAudioContentBannerResponse,
position: Int,
pageSize: Int
) {
val ivBanner = holder.findViewById<ImageView>(R.id.iv_recommend_live)
val layoutParams = ivBanner.layoutParams as FrameLayout.LayoutParams
layoutParams.width = itemWidth
layoutParams.height = itemHeight
ivBanner.load(data.thumbnailImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
transformations(RoundedCornersTransformation(5.3f.dpToPx()))
}
ivBanner.layoutParams = layoutParams
ivBanner.setOnClickListener { onClick(data) }
}
override fun getLayoutId(viewType: Int): Int {
return R.layout.item_recommend_live
}
}

View File

@ -0,0 +1,41 @@
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()
}
}

View File

@ -0,0 +1,94 @@
package kr.co.vividnext.sodalive.audio_content.main
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Rect
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.databinding.ItemAudioContentMainCurationBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
class AudioContentMainCurationAdapter(
private val onClickItem: (Long) -> Unit,
private val onClickCreator: (Long) -> Unit,
) : RecyclerView.Adapter<AudioContentMainCurationAdapter.ViewHolder>() {
private val items = mutableListOf<GetAudioContentCurationResponse>()
inner class ViewHolder(
private val context: Context,
private val binding: ItemAudioContentMainCurationBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetAudioContentCurationResponse) {
binding.tvTitle.text = item.title
binding.tvDesc.text = item.description
setAudioContentList(item.audioContents)
}
private fun setAudioContentList(audioContents: List<GetAudioContentMainItem>) {
val adapter = AudioContentMainContentAdapter(onClickItem, onClickCreator)
binding.rvCuration.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
false
)
if (binding.rvCuration.itemDecorationCount == 0) {
binding.rvCuration.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 6.7f.dpToPx().toInt()
}
adapter.itemCount - 1 -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 6.7f.dpToPx().toInt()
}
}
}
})
}
binding.rvCuration.adapter = adapter
adapter.addItems(audioContents)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
parent.context,
ItemAudioContentMainCurationBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
}
override fun getItemCount() = items.size
@SuppressLint("NotifyDataSetChanged")
fun addItems(items: List<GetAudioContentCurationResponse>) {
this.items.clear()
this.items.addAll(items)
notifyDataSetChanged()
}
}

View File

@ -0,0 +1,470 @@
package kr.co.vividnext.sodalive.audio_content.main
import android.app.Service
import android.content.Intent
import android.graphics.Rect
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.LinearLayout
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.zhpan.bannerview.BaseBannerAdapter
import com.zhpan.indicator.enums.IndicatorSlideMode
import com.zhpan.indicator.enums.IndicatorStyle
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.order.AudioContentOrderListActivity
import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadActivity
import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.FragmentAudioContentMainBinding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.settings.event.EventDetailActivity
import kr.co.vividnext.sodalive.settings.notification.MemberRole
import org.koin.android.ext.android.inject
import kotlin.math.roundToInt
class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
FragmentAudioContentMainBinding::inflate
) {
private val viewModel: AudioContentMainViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var imm: InputMethodManager
private lateinit var newContentCreatorAdapter: AudioContentMainNewContentCreatorAdapter
private lateinit var bannerAdapter: AudioContentMainBannerAdapter
private lateinit var orderListAdapter: AudioContentMainContentAdapter
private lateinit var newContentThemeAdapter: AudioContentMainNewContentThemeAdapter
private lateinit var newContentAdapter: AudioContentMainContentAdapter
private lateinit var curationAdapter: AudioContentMainCurationAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
imm = requireContext().getSystemService(
Service.INPUT_METHOD_SERVICE
) as InputMethodManager
setupView()
bindData()
viewModel.getMain()
}
private fun setupView() {
if (SharedPreferenceManager.role == MemberRole.CREATOR.name) {
binding.llUploadContent.visibility = View.VISIBLE
binding.llUploadContent.setOnClickListener {
startActivity(
Intent(
requireActivity(),
AudioContentUploadActivity::class.java
)
)
}
} else {
binding.llUploadContent.visibility = View.GONE
}
setupNewContentCreator()
setupBanner()
setupOrderList()
setupNewContentTheme()
setupNewContent()
setupCuration()
binding.swipeRefreshLayout.setOnRefreshListener {
binding.swipeRefreshLayout.isRefreshing = false
viewModel.getMain()
}
}
private fun setupNewContentCreator() {
newContentCreatorAdapter = AudioContentMainNewContentCreatorAdapter {
val intent = Intent(requireContext(), UserProfileActivity::class.java)
intent.putExtra(Constants.EXTRA_USER_ID, it)
startActivity(intent)
}
binding.rvNewContentCreator.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
false
)
binding.rvNewContentCreator.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 10.7f.dpToPx().toInt()
}
orderListAdapter.itemCount - 1 -> {
outRect.left = 10.7f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 10.7f.dpToPx().toInt()
outRect.right = 10.7f.dpToPx().toInt()
}
}
}
})
binding.rvNewContentCreator.adapter = newContentCreatorAdapter
}
private fun setupBanner() {
val layoutParams = binding
.rvBanner
.layoutParams as LinearLayout.LayoutParams
val pagerWidth = screenWidth.toDouble() - 26.7f.dpToPx()
val pagerHeight = (pagerWidth * 0.53).roundToInt()
layoutParams.width = pagerWidth.roundToInt()
layoutParams.height = pagerHeight
bannerAdapter = AudioContentMainBannerAdapter(pagerWidth.roundToInt(), pagerHeight) {
when (it.type) {
AudioContentBannerType.EVENT -> {
startActivity(
Intent(requireContext(), EventDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_EVENT, it.eventItem!!)
}
)
}
AudioContentBannerType.CREATOR -> {
startActivity(
Intent(requireContext(), UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, it.creatorId!!)
}
)
}
AudioContentBannerType.LINK -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it.link!!)))
}
}
}
binding
.rvBanner
.layoutParams = layoutParams
binding.rvBanner.apply {
adapter = bannerAdapter as BaseBannerAdapter<Any>
setLifecycleRegistry(lifecycle)
setScrollDuration(1000)
setInterval(4 * 1000)
}.create()
binding
.rvBanner
.setIndicatorView(binding.indicatorBanner)
.setIndicatorStyle(IndicatorStyle.ROUND_RECT)
.setIndicatorSlideMode(IndicatorSlideMode.SMOOTH)
.setIndicatorVisibility(View.GONE)
.setIndicatorSliderColor(
ContextCompat.getColor(requireContext(), R.color.color_909090),
ContextCompat.getColor(requireContext(), R.color.color_9970ff)
)
.setIndicatorSliderWidth(4f.dpToPx().toInt(), 10f.dpToPx().toInt())
.setIndicatorHeight(4f.dpToPx().toInt())
}
private fun setupOrderList() {
orderListAdapter = AudioContentMainContentAdapter(
onClickItem = {
startActivity(
Intent(requireContext(), AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
}
)
},
onClickCreator = {
startActivity(
Intent(requireContext(), UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, it)
}
)
}
)
binding.rvMyStash.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
false
)
binding.rvMyStash.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 6.7f.dpToPx().toInt()
}
orderListAdapter.itemCount - 1 -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 6.7f.dpToPx().toInt()
}
}
}
})
binding.rvMyStash.adapter = orderListAdapter
binding.tvMyStashViewAll.setOnClickListener {
startActivity(Intent(requireContext(), AudioContentOrderListActivity::class.java))
}
}
private fun setupNewContentTheme() {
newContentThemeAdapter = AudioContentMainNewContentThemeAdapter {
viewModel.getNewContentOfTheme(theme = it)
}
binding.rvNewContentTheme.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
false
)
binding.rvNewContentTheme.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 4f.dpToPx().toInt()
}
orderListAdapter.itemCount - 1 -> {
outRect.left = 4f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 4f.dpToPx().toInt()
outRect.right = 4f.dpToPx().toInt()
}
}
}
})
binding.rvNewContentTheme.adapter = newContentThemeAdapter
}
private fun setupNewContent() {
newContentAdapter = AudioContentMainContentAdapter(
onClickItem = {
startActivity(
Intent(requireContext(), AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
}
)
},
onClickCreator = {
startActivity(
Intent(requireContext(), UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, it)
}
)
}
)
binding.rvNewContent.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
false
)
binding.rvNewContent.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 6.7f.dpToPx().toInt()
}
orderListAdapter.itemCount - 1 -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 6.7f.dpToPx().toInt()
}
}
}
})
binding.rvNewContent.adapter = newContentAdapter
}
private fun setupCuration() {
curationAdapter = AudioContentMainCurationAdapter(
onClickItem = {
startActivity(
Intent(requireContext(), AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
}
)
},
onClickCreator = {
startActivity(
Intent(requireContext(), UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, it)
}
)
}
)
binding.rvCuration.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.VERTICAL,
false
)
binding.rvCuration.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.top = 40f.dpToPx().toInt()
outRect.bottom = 20f.dpToPx().toInt()
}
curationAdapter.itemCount - 1 -> {
outRect.top = 20f.dpToPx().toInt()
outRect.bottom = 40f.dpToPx().toInt()
}
else -> {
outRect.top = 20f.dpToPx().toInt()
outRect.bottom = 20f.dpToPx().toInt()
}
}
}
})
binding.rvCuration.adapter = curationAdapter
}
private fun bindData() {
viewModel.isLoading.observe(viewLifecycleOwner) {
if (it) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
}
}
viewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
}
viewModel.newContentUploadCreatorListLiveData.observe(viewLifecycleOwner) {
newContentCreatorAdapter.addItems(it)
binding.rvNewContentCreator.visibility = if (
newContentCreatorAdapter.itemCount <= 0 && it.isEmpty()
) {
View.GONE
} else {
View.VISIBLE
}
}
viewModel.bannerLiveData.observe(viewLifecycleOwner) {
if (bannerAdapter.itemCount <= 0 && it.isEmpty()) {
binding.rvBanner.visibility = View.GONE
binding.indicatorBanner.visibility = View.GONE
} else {
binding.rvBanner.visibility = View.VISIBLE
binding.indicatorBanner.visibility = View.VISIBLE
binding.rvBanner.refreshData(it)
}
}
viewModel.orderListLiveData.observe(viewLifecycleOwner) {
orderListAdapter.addItems(it)
binding.llMyStash.visibility = if (
orderListAdapter.itemCount <= 0 && it.isEmpty()
) {
View.GONE
} else {
View.VISIBLE
}
}
viewModel.newContentListLiveData.observe(viewLifecycleOwner) {
newContentAdapter.addItems(it)
}
viewModel.themeListLiveData.observe(viewLifecycleOwner) {
binding.llNewContent.visibility = View.VISIBLE
newContentThemeAdapter.addItems(it)
}
viewModel.curationListLiveData.observe(viewLifecycleOwner) {
curationAdapter.addItems(it)
binding.rvCuration.visibility = if (
curationAdapter.itemCount <= 0 && it.isEmpty()
) {
View.GONE
} else {
View.VISIBLE
}
}
}
}

View File

@ -0,0 +1,41 @@
package kr.co.vividnext.sodalive.audio_content.main
import android.view.View
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_logo)
transformations(RoundedCornersTransformation(2.7f.dpToPx()))
}
binding.ivAudioContentCreator.load(item.creatorProfileImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
transformations(CircleCropTransformation())
}
binding.tvAudioContentTitle.text = item.title
binding.tvAudioContentCreatorNickname.text = item.creatorNickname
binding.ivAudioContentCreator.setOnClickListener { onClickCreator(item.creatorId) }
binding.iv19.visibility = if (item.isAdult) {
View.VISIBLE
} else {
View.GONE
}
binding.root.setOnClickListener { onClickItem(item.contentId) }
}
}

View File

@ -0,0 +1,52 @@
package kr.co.vividnext.sodalive.audio_content.main
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemAudioContentMainNewContentCreatorBinding
class AudioContentMainNewContentCreatorAdapter(
private val onClickItem: (Long) -> Unit
) : RecyclerView.Adapter<AudioContentMainNewContentCreatorAdapter.ViewHolder>() {
private val items = mutableListOf<GetNewContentUploadCreator>()
inner class ViewHolder(
private val binding: ItemAudioContentMainNewContentCreatorBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetNewContentUploadCreator) {
binding.tvNewContentCreator.text = item.creatorNickname
binding.ivNewContentCreator.load(item.creatorProfileImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
transformations(CircleCropTransformation())
}
binding.root.setOnClickListener { onClickItem(item.creatorId) }
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
ItemAudioContentMainNewContentCreatorBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun getItemCount() = items.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
}
@SuppressLint("NotifyDataSetChanged")
fun addItems(items: List<GetNewContentUploadCreator>) {
this.items.clear()
this.items.addAll(items)
notifyDataSetChanged()
}
}

View File

@ -0,0 +1,68 @@
package kr.co.vividnext.sodalive.audio_content.main
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemAudioContentMainNewContentThemeBinding
class AudioContentMainNewContentThemeAdapter(
private val onClickItem: (String) -> Unit
) : RecyclerView.Adapter<AudioContentMainNewContentThemeAdapter.ViewHolder>() {
private val themeList = mutableListOf<String>()
private var selectedTheme = ""
inner class ViewHolder(
private val context: Context,
private val binding: ItemAudioContentMainNewContentThemeBinding
) : RecyclerView.ViewHolder(binding.root) {
@SuppressLint("NotifyDataSetChanged")
fun bind(theme: String) {
if (theme == selectedTheme || (selectedTheme == "" && theme == "전체")) {
binding.tvTheme.setBackgroundResource(
R.drawable.bg_round_corner_16_7_transparent_9970ff
)
binding.tvTheme.setTextColor(ContextCompat.getColor(context, R.color.color_9970ff))
} else {
binding.tvTheme.setBackgroundResource(
R.drawable.bg_round_corner_16_7_transparent_777777
)
binding.tvTheme.setTextColor(ContextCompat.getColor(context, R.color.color_777777))
}
binding.tvTheme.text = theme
binding.root.setOnClickListener {
onClickItem(theme)
selectedTheme = theme
notifyDataSetChanged()
}
}
}
@SuppressLint("NotifyDataSetChanged")
fun addItems(themeList: List<String>) {
this.selectedTheme = ""
this.themeList.clear()
this.themeList.addAll(themeList)
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
parent.context,
ItemAudioContentMainNewContentThemeBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun getItemCount() = themeList.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(themeList[position])
}
}

View File

@ -0,0 +1,121 @@
package kr.co.vividnext.sodalive.audio_content.main
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.audio_content.AudioContentRepository
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
class AudioContentMainViewModel(
private val repository: AudioContentRepository
) : BaseViewModel() {
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
private var _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean>
get() = _isLoading
private var _newContentUploadCreatorListLiveData =
MutableLiveData<List<GetNewContentUploadCreator>>()
val newContentUploadCreatorListLiveData: LiveData<List<GetNewContentUploadCreator>>
get() = _newContentUploadCreatorListLiveData
private var _newContentListLiveData = MutableLiveData<List<GetAudioContentMainItem>>()
val newContentListLiveData: LiveData<List<GetAudioContentMainItem>>
get() = _newContentListLiveData
private var _bannerLiveData = MutableLiveData<List<GetAudioContentBannerResponse>>()
val bannerLiveData: LiveData<List<GetAudioContentBannerResponse>>
get() = _bannerLiveData
private var _orderListLiveData = MutableLiveData<List<GetAudioContentMainItem>>()
val orderListLiveData: LiveData<List<GetAudioContentMainItem>>
get() = _orderListLiveData
private var _themeListLiveData = MutableLiveData<List<String>>()
val themeListLiveData: LiveData<List<String>>
get() = _themeListLiveData
private var _curationListLiveData = MutableLiveData<List<GetAudioContentCurationResponse>>()
val curationListLiveData: LiveData<List<GetAudioContentCurationResponse>>
get() = _curationListLiveData
fun getMain() {
_isLoading.value = true
compositeDisposable.add(
repository.getMain(token = "Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
val data = it.data
_newContentUploadCreatorListLiveData.value =
data.newContentUploadCreatorList
_newContentListLiveData.value = data.newContentList
_orderListLiveData.value = data.orderList
_bannerLiveData.value = data.bannerList
_curationListLiveData.value = data.curationList
val themeList = listOf("전체").union(data.themeList).toList()
_themeListLiveData.value = themeList
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
fun getNewContentOfTheme(theme: String) {
compositeDisposable.add(
repository.getNewContentOfTheme(
theme = if (theme == "전체") {
""
} else {
theme
},
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
_newContentListLiveData.value = it.data!!
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
},
{
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
}

View File

@ -0,0 +1,55 @@
package kr.co.vividnext.sodalive.audio_content.main
import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.settings.event.EventItem
data class GetAudioContentMainResponse(
@SerializedName("newContentUploadCreatorList")
val newContentUploadCreatorList: List<GetNewContentUploadCreator>,
@SerializedName("bannerList") val bannerList: List<GetAudioContentBannerResponse>,
@SerializedName("orderList") val orderList: List<GetAudioContentMainItem>,
@SerializedName("themeList") val themeList: List<String>,
@SerializedName("newContentList") val newContentList: List<GetAudioContentMainItem>,
@SerializedName("curationList") val curationList: List<GetAudioContentCurationResponse>
)
data class GetNewContentUploadCreator(
@SerializedName("creatorId") val creatorId: Long,
@SerializedName("creatorNickname") val creatorNickname: String,
@SerializedName("creatorProfileImageUrl") val creatorProfileImageUrl: String
)
data class GetAudioContentMainItem(
@SerializedName("contentId") val contentId: Long,
@SerializedName("coverImageUrl") val coverImageUrl: String,
@SerializedName("title") val title: String,
@SerializedName("isAdult") val isAdult: Boolean,
@SerializedName("creatorId") val creatorId: Long,
@SerializedName("creatorProfileImageUrl") val creatorProfileImageUrl: String,
@SerializedName("creatorNickname") val creatorNickname: String
)
data class GetAudioContentCurationResponse(
@SerializedName("title") val title: String,
@SerializedName("description") val description: String,
@SerializedName("audioContents") val audioContents: List<GetAudioContentMainItem>
)
data class GetAudioContentBannerResponse(
@SerializedName("type") val type: AudioContentBannerType,
@SerializedName("thumbnailImageUrl") val thumbnailImageUrl: String,
@SerializedName("eventItem") val eventItem: EventItem?,
@SerializedName("creatorId") val creatorId: Long?,
@SerializedName("link") val link: String?
)
enum class AudioContentBannerType {
@SerializedName("EVENT")
EVENT,
@SerializedName("CREATOR")
CREATOR,
@SerializedName("LINK")
LINK
}

View File

@ -0,0 +1,288 @@
package kr.co.vividnext.sodalive.audio_content.modify
import android.Manifest
import android.annotation.SuppressLint
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.gun0912.tedpermission.PermissionListener
import com.gun0912.tedpermission.normal.TedPermission
import com.jakewharton.rxbinding4.widget.textChanges
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
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.LoadingDialog
import kr.co.vividnext.sodalive.common.RealPathUtil
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentModifyBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
class AudioContentModifyActivity : BaseActivity<ActivityAudioContentModifyBinding>(
ActivityAudioContentModifyBinding::inflate
) {
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()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val audioContentId = intent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0)
if (audioContentId <= 0) {
Toast.makeText(applicationContext, "잘못된 요청입니다.", Toast.LENGTH_LONG).show()
finish()
}
checkPermissions()
viewModel.getRealPathFromURI = {
RealPathUtil.getRealPath(applicationContext, it)
}
bindData()
viewModel.getAudioContentDetail(audioContentId = audioContentId) { finish() }
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
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.llCommentNo.setOnClickListener { viewModel.setAvailableComment(false) }
binding.llCommentYes.setOnClickListener { viewModel.setAvailableComment(true) }
binding.tvCancel.setOnClickListener { finish() }
binding.tvModify.setOnClickListener {
viewModel.modifyAudioContent { finish() }
}
}
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)
} else {
listOf(Manifest.permission.READ_EXTERNAL_STORAGE)
}
TedPermission.create()
.setPermissionListener(object : PermissionListener {
override fun onPermissionGranted() {
}
override fun onPermissionDenied(deniedPermissions: MutableList<String>?) {
finish()
}
})
.setDeniedMessage(R.string.read_storage_permission_denied_message)
.setPermissions(*permissions.toTypedArray())
.check()
}
@SuppressLint("SetTextI18n")
private fun bindData() {
compositeDisposable.add(
binding.etTitle.textChanges().skip(1)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
viewModel.title = it.toString()
}
)
compositeDisposable.add(
binding.etDetail.textChanges().skip(1)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
binding.tvNumberOfCharacters.text = "${it.length}"
viewModel.detail = it.toString()
}
)
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.isAvailableCommentLiveData.observe(this) {
if (it) {
binding.ivCommentYes.visibility = View.VISIBLE
binding.tvCommentYes.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_eeeeee
)
)
binding.llCommentYes.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.ivCommentNo.visibility = View.GONE
binding.tvCommentNo.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
)
)
binding.llCommentNo.setBackgroundResource(
R.drawable.bg_round_corner_6_7_1f1734_9970ff
)
} else {
binding.ivCommentNo.visibility = View.VISIBLE
binding.tvCommentNo.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_eeeeee
)
)
binding.llCommentNo.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.ivCommentYes.visibility = View.GONE
binding.tvCommentYes.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
)
)
binding.llCommentYes
.setBackgroundResource(R.drawable.bg_round_corner_6_7_1f1734_9970ff)
}
}
viewModel.isAdultShowUiLiveData.observe(this) {
if (it) {
binding.llSetAdult.visibility = View.VISIBLE
binding.llAgeAll.setOnClickListener {
viewModel.setAdult(false)
}
binding.llAge19.setOnClickListener {
viewModel.setAdult(true)
}
viewModel.isAdultLiveData.observe(this) {
if (it) {
binding.ivAgeAll.visibility = View.GONE
binding.llAgeAll.setBackgroundResource(
R.drawable.bg_round_corner_6_7_1f1734
)
binding.tvAgeAll.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
)
)
binding.ivAge19.visibility = View.VISIBLE
binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.tvAge19.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_eeeeee
)
)
} else {
binding.ivAge19.visibility = View.GONE
binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_1f1734)
binding.tvAge19.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
)
)
binding.ivAgeAll.visibility = View.VISIBLE
binding.llAgeAll.setBackgroundResource(
R.drawable.bg_round_corner_6_7_9970ff
)
binding.tvAgeAll.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_eeeeee
)
)
}
}
} else {
binding.llSetAdult.visibility = View.GONE
}
}
viewModel.coverImageLiveData.observe(this) {
binding.ivCover.setPadding(0)
binding.ivCover.background = null
binding.ivCover.load(it) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(RoundedCornersTransformation(13.3f.dpToPx()))
}
}
viewModel.titleLiveData.observe(this) {
binding.etTitle.setText(it)
}
viewModel.detailLiveData.observe(this) {
binding.etDetail.setText(it)
}
}
}

View File

@ -0,0 +1,212 @@
package kr.co.vividnext.sodalive.audio_content.modify
import android.net.Uri
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.google.gson.Gson
import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.audio_content.AudioContentRepository
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import okio.BufferedSink
import java.io.File
class AudioContentModifyViewModel(
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 val _isAdultLiveData = MutableLiveData(false)
val isAdultLiveData: LiveData<Boolean>
get() = _isAdultLiveData
private val _isAvailableCommentLiveData = MutableLiveData(false)
val isAvailableCommentLiveData: LiveData<Boolean>
get() = _isAvailableCommentLiveData
private val _titleLiveData = MutableLiveData("")
val titleLiveData: LiveData<String>
get() = _titleLiveData
private val _detailLiveData = MutableLiveData("")
val detailLiveData: LiveData<String>
get() = _detailLiveData
private val _coverImageLiveData = MutableLiveData("")
val coverImageLiveData: LiveData<String>
get() = _coverImageLiveData
private val _isAdultShowUiLiveData = MutableLiveData(true)
val isAdultShowUiLiveData: LiveData<Boolean>
get() = _isAdultShowUiLiveData
lateinit var getRealPathFromURI: (Uri) -> String?
var contentId: Long = 0
var title: String? = null
var detail: String? = null
var coverImageUri: Uri? = null
fun setAdult(isAdult: Boolean) {
_isAdultLiveData.postValue(isAdult)
}
fun setAvailableComment(isAvailableComment: Boolean) {
_isAvailableCommentLiveData.postValue(isAvailableComment)
}
fun getAudioContentDetail(audioContentId: Long, onFailure: (() -> Unit)? = null) {
this.contentId = audioContentId
_isLoading.value = true
compositeDisposable.add(
repository.getAudioContentDetail(
audioContentId = audioContentId,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
_titleLiveData.value = it.data.title
_detailLiveData.value = it.data.detail
_coverImageLiveData.value = it.data.coverImageUrl
_isAvailableCommentLiveData.value = it.data.isCommentAvailable
_isAdultLiveData.value = it.data.isAdult
_isAdultShowUiLiveData.value = !it.data.isAdult
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
if (onFailure != null) {
onFailure()
}
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
if (onFailure != null) {
onFailure()
}
}
)
)
}
fun modifyAudioContent(onSuccess: () -> Unit) {
if (!_isLoading.value!! && contentId > 0 && validateData()) {
_isLoading.value = true
val request = ModifyAudioContentRequest(
contentId = contentId,
title = title,
detail = detail,
isAdult = _isAdultLiveData.value!!,
isCommentAvailable = _isAvailableCommentLiveData.value!!
)
val requestJson = Gson().toJson(request)
val coverImage = if (coverImageUri != null) {
val file = File(getRealPathFromURI(coverImageUri!!))
MultipartBody.Part.createFormData(
"coverImage",
file.name,
body = object : RequestBody() {
override fun contentType(): MediaType {
return "image/*".toMediaType()
}
override fun writeTo(sink: BufferedSink) {
file.inputStream().use { inputStream ->
val buffer = ByteArray(1024)
var bytesRead: Int
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
sink.write(buffer, 0, bytesRead)
}
}
}
override fun contentLength(): Long {
return file.length()
}
}
)
} else {
null
}
compositeDisposable.add(
repository.modifyAudioContent(
coverImage = coverImage,
request = requestJson.toRequestBody("text/plain".toMediaType()),
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success) {
_toastLiveData.postValue("수정되었습니다.")
onSuccess()
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
_isLoading.postValue(false)
},
{
_isLoading.postValue(false)
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
)
)
}
}
private fun validateData(): Boolean {
if (title != null && title!!.isBlank()) {
_toastLiveData.postValue("제목을 입력해 주세요.")
return false
}
if (detail != null && (detail!!.isBlank() || detail!!.length < 5)) {
_toastLiveData.postValue("내용을 5자 이상 입력해 주세요.")
return false
}
return true
}
}

View File

@ -0,0 +1,11 @@
package kr.co.vividnext.sodalive.audio_content.modify
import com.google.gson.annotations.SerializedName
data class ModifyAudioContentRequest(
@SerializedName("contentId") val contentId: Long,
@SerializedName("title") val title: String?,
@SerializedName("detail") val detail: String?,
@SerializedName("isAdult") val isAdult: Boolean,
@SerializedName("isCommentAvailable") val isCommentAvailable: Boolean
)

View File

@ -0,0 +1,100 @@
package kr.co.vividnext.sodalive.audio_content.order
import android.app.Activity
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.LayoutInflater
import android.view.View
import android.view.WindowManager
import androidx.appcompat.app.AlertDialog
import coil.load
import coil.transform.CircleCropTransformation
import coil.transform.RoundedCornersTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.DialogAudioContentOrderConfirmBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kotlin.math.ceil
class AudioContentOrderConfirmDialog(
activity: Activity,
layoutInflater: LayoutInflater,
title: String,
theme: String,
coverImageUrl: String,
isAdult: Boolean,
profileImageUrl: String,
nickname: String,
duration: String,
orderType: OrderType,
price: Int,
confirmButtonClick: () -> Unit,
) {
private val alertDialog: AlertDialog
val dialogView = DialogAudioContentOrderConfirmBinding.inflate(layoutInflater)
init {
val dialogBuilder = AlertDialog.Builder(activity)
dialogBuilder.setView(dialogView.root)
alertDialog = dialogBuilder.create()
alertDialog.setCancelable(false)
alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
dialogView.tvTitle.text = title
dialogView.tvTheme.text = theme
dialogView.tvProfileNickname.text = nickname
dialogView.ivCover.load(coverImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
transformations(RoundedCornersTransformation(4f))
}
dialogView.ivProfile.load(profileImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
transformations(CircleCropTransformation())
}
dialogView.tvDuration.text = duration
dialogView.tvPrice.text = if (orderType == OrderType.RENTAL) {
"${ceil(price * 0.7).toInt()}"
} else {
"$price"
}
dialogView.iv19.visibility = if (isAdult) {
View.VISIBLE
} else {
View.GONE
}
dialogView.tvNotice.text = if (orderType == OrderType.RENTAL) {
"콘텐츠를 대여하시겠습니까?\n아래 코인이 차감됩니다."
} else {
"콘텐츠를 소장하시겠습니까?\n아래 코인이 차감됩니다."
}
dialogView.tvCancel.setOnClickListener {
alertDialog.dismiss()
}
dialogView.tvConfirm.setOnClickListener {
alertDialog.dismiss()
confirmButtonClick()
}
}
fun show(width: Int) {
alertDialog.show()
val lp = WindowManager.LayoutParams()
lp.copyFrom(alertDialog.window?.attributes)
lp.width = width - (26.7f.dpToPx()).toInt()
lp.height = WindowManager.LayoutParams.WRAP_CONTENT
alertDialog.window?.attributes = lp
}
}

View File

@ -0,0 +1,44 @@
package kr.co.vividnext.sodalive.audio_content.order
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import kr.co.vividnext.sodalive.databinding.FragmentAudioContentOrderBinding
import kotlin.math.ceil
class AudioContentOrderFragment(
private val price: Int,
private val onClickRental: () -> Unit,
private val onClickKeep: () -> Unit
) : BottomSheetDialogFragment() {
private lateinit var binding: FragmentAudioContentOrderBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentAudioContentOrderBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.tvKeep.text = "$price"
binding.tvRental.text = "${ceil(price * 0.7).toInt()}"
binding.llKeep.setOnClickListener {
onClickKeep()
dismiss()
}
binding.llRental.setOnClickListener {
onClickRental()
dismiss()
}
}
}

View File

@ -0,0 +1,110 @@
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 android.widget.Toast
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.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() }
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.text = "구매목록"
binding.toolbar.tvBack.setOnClickListener { finish() }
adapter = AudioContentOrderListAdapter {
startActivity(
Intent(applicationContext, AudioContentDetailActivity::class.java)
.apply { putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it) }
)
}
binding.rvOrderList.layoutManager = LinearLayoutManager(
applicationContext,
LinearLayoutManager.VERTICAL,
false
)
binding.rvOrderList.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
outRect.left = 13.3f.dpToPx().toInt()
outRect.right = 13.3f.dpToPx().toInt()
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.top = 13.3f.dpToPx().toInt()
outRect.bottom = 6.7f.dpToPx().toInt()
}
adapter.itemCount - 1 -> {
outRect.top = 6.7f.dpToPx().toInt()
outRect.bottom = 13.3f.dpToPx().toInt()
}
else -> {
outRect.top = 6.7f.dpToPx().toInt()
outRect.bottom = 6.7f.dpToPx().toInt()
}
}
}
})
binding.rvOrderList.adapter = adapter
}
@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()
}
}
}

View File

@ -0,0 +1,61 @@
package kr.co.vividnext.sodalive.audio_content.order
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.ItemAudioContentOrderListBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.moneyFormat
class AudioContentOrderListAdapter(
private val onItemClick: (Long) -> Unit
) : RecyclerView.Adapter<AudioContentOrderListAdapter.ViewHolder>() {
var items = mutableSetOf<GetAudioContentOrderListItem>()
inner class ViewHolder(
private val binding: ItemAudioContentOrderListBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetAudioContentOrderListItem) {
binding.ivCover.load(item.coverImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
transformations(RoundedCornersTransformation(5.3f.dpToPx()))
}
binding.tvTitle.text = item.title
binding.tvTheme.text = item.themeStr
binding.tvDuration.text = item.duration
binding.tvLikeCount.text = item.likeCount.moneyFormat()
binding.tvCommentCount.text = item.commentCount.moneyFormat()
if (item.orderType == OrderType.RENTAL) {
binding.tvRental.visibility = View.VISIBLE
binding.tvPurchased.visibility = View.GONE
} else {
binding.tvPurchased.visibility = View.VISIBLE
binding.tvRental.visibility = View.GONE
}
binding.root.setOnClickListener { onItemClick(item.contentId) }
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
ItemAudioContentOrderListBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items.toList()[position])
}
override fun getItemCount() = items.size
}

View File

@ -0,0 +1,77 @@
package kr.co.vividnext.sodalive.audio_content.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.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
class AudioContentOrderListViewModel(
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 _orderList = MutableLiveData<List<GetAudioContentOrderListItem>>()
val orderList: LiveData<List<GetAudioContentOrderListItem>>
get() = _orderList
private var isLast = false
var page = 1
private val size = 10
fun getAudioContentOrderList(onFailure: (() -> Unit)? = null) {
_isLoading.value = true
compositeDisposable.add(
repository.getAudioContentOrderList(
page = page,
size = size,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
if (it.data.items.isNotEmpty()) {
page += 1
_orderList.postValue(it.data.items)
} else {
isLast = true
}
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
if (onFailure != null) {
onFailure()
}
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
if (onFailure != null) {
onFailure()
}
}
)
)
}
}

View File

@ -0,0 +1,20 @@
package kr.co.vividnext.sodalive.audio_content.order
import com.google.gson.annotations.SerializedName
data class GetAudioContentOrderListResponse(
@SerializedName("totalCount") val totalCount: Int,
@SerializedName("items") val items: List<GetAudioContentOrderListItem>
)
data class GetAudioContentOrderListItem(
@SerializedName("contentId") val contentId: Long,
@SerializedName("coverImageUrl") val coverImageUrl: String,
@SerializedName("title") val title: String,
@SerializedName("themeStr") val themeStr: String,
@SerializedName("duration") val duration: String?,
@SerializedName("isAdult") val isAdult: Boolean,
@SerializedName("orderType") val orderType: OrderType,
@SerializedName("likeCount") val likeCount: Int,
@SerializedName("commentCount") val commentCount: Int,
)

View File

@ -0,0 +1,9 @@
package kr.co.vividnext.sodalive.audio_content.order
import com.google.gson.annotations.SerializedName
data class OrderRequest(
@SerializedName("contentId") val contentId: Long,
@SerializedName("orderType") val orderType: OrderType,
@SerializedName("container") val container: String
)

View File

@ -0,0 +1,11 @@
package kr.co.vividnext.sodalive.audio_content.order
import com.google.gson.annotations.SerializedName
enum class OrderType {
@SerializedName("RENTAL")
RENTAL,
@SerializedName("KEEP")
KEEP
}

View File

@ -0,0 +1,435 @@
package kr.co.vividnext.sodalive.audio_content.upload
import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.OpenableColumns
import android.view.View
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import coil.load
import coil.transform.CircleCropTransformation
import coil.transform.RoundedCornersTransformation
import com.github.dhaval2404.imagepicker.ImagePicker
import com.gun0912.tedpermission.PermissionListener
import com.gun0912.tedpermission.normal.TedPermission
import com.jakewharton.rxbinding4.widget.textChanges
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.upload.theme.AudioContentThemeFragment
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.RealPathUtil
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentUploadBinding
import kr.co.vividnext.sodalive.dialog.LiveDialog
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
import java.io.File
class AudioContentUploadActivity : BaseActivity<ActivityAudioContentUploadBinding>(
ActivityAudioContentUploadBinding::inflate
) {
private val viewModel: AudioContentUploadViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private val themeFragment: AudioContentThemeFragment by lazy {
AudioContentThemeFragment(
getSelectedTheme = { viewModel.theme },
onItemClick = {
binding.ivTheme.load(it.image) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(CircleCropTransformation())
binding.ivTheme.visibility = View.VISIBLE
}
binding.tvTheme.text = it.theme
viewModel.theme = it
}
)
}
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.background = null
binding.ivCover.load(fileUri) {
crossfade(true)
placeholder(R.drawable.ic_logo)
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 val selectAudioActivityResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
val resultCode = result.resultCode
val data = result.data
if (resultCode == RESULT_OK) {
val fileUri = data?.data
if (fileUri != null) {
binding.tvSelectContent.text = getFileName(fileUri)
viewModel.contentUri = fileUri
} else {
Toast.makeText(
this,
"잘못된 파일입니다.\n다시 선택해 주세요.",
Toast.LENGTH_SHORT
).show()
}
} else if (resultCode == ImagePicker.RESULT_ERROR) {
binding.tvSelectContent.text = "파일 선택"
viewModel.contentUri = null
Toast.makeText(this, ImagePicker.getError(data), Toast.LENGTH_SHORT).show()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
checkPermissions()
viewModel.getRealPathFromURI = {
RealPathUtil.getRealPath(applicationContext, it)
}
bindData()
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.text = "콘텐츠 등록"
binding.toolbar.tvBack.setOnClickListener { finish() }
binding.llTheme.setOnClickListener {
if (themeFragment.isAdded) return@setOnClickListener
themeFragment.show(supportFragmentManager, themeFragment.tag)
}
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.tvSelectContent.setOnClickListener {
val intent = Intent().apply {
type = "audio/*"
action = Intent.ACTION_GET_CONTENT
addCategory(Intent.CATEGORY_OPENABLE)
}
selectAudioActivityResultLauncher.launch(
Intent.createChooser(
intent,
"Select Audio"
)
)
}
if (SharedPreferenceManager.isAuth) {
binding.llSetAdult.visibility = View.VISIBLE
} else {
binding.llSetAdult.visibility = View.GONE
}
binding.llPricePaid.setOnClickListener { viewModel.setPriceFree(false) }
binding.llPriceFree.setOnClickListener { viewModel.setPriceFree(true) }
binding.llCommentNo.setOnClickListener { viewModel.setAvailableComment(false) }
binding.llCommentYes.setOnClickListener { viewModel.setAvailableComment(true) }
binding.tvCancel.setOnClickListener { finish() }
binding.tvUpload.setOnClickListener {
viewModel.uploadAudioContent {
LiveDialog(
activity = this,
layoutInflater = layoutInflater,
title = "콘텐츠 업로드",
desc = "등록한 콘텐츠가 업로드 중입니다.\n" +
"콘텐츠 등록이 완료되면 알림을 보내드립니다.\n" +
"이 페이지를 나가도 콘텐츠는 자동으로 등록됩니다.",
confirmButtonTitle = "확인",
confirmButtonClick = { finish() },
).show(screenWidth)
}
}
}
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)
} else {
listOf(Manifest.permission.READ_EXTERNAL_STORAGE)
}
TedPermission.create()
.setPermissionListener(object : PermissionListener {
override fun onPermissionGranted() {
}
override fun onPermissionDenied(deniedPermissions: MutableList<String>?) {
finish()
}
})
.setDeniedMessage(R.string.read_storage_permission_denied_message)
.setPermissions(*permissions.toTypedArray())
.check()
}
@SuppressLint("SetTextI18n")
private fun bindData() {
compositeDisposable.add(
binding.etTitle.textChanges().skip(1)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
viewModel.title = it.toString()
}
)
compositeDisposable.add(
binding.etDetail.textChanges().skip(1)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
binding.tvNumberOfCharacters.text = "${it.length}"
viewModel.detail = it.toString()
}
)
compositeDisposable.add(
binding.etTag.textChanges().skip(1)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
viewModel.tags = it.toString()
}
)
compositeDisposable.add(
binding.etSetPrice.textChanges().skip(1)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
val price = it.toString().toIntOrNull()
if (price != null) {
viewModel.price = price.toInt()
} else {
viewModel.price = 0
if (it.isNotBlank()) {
binding.etSetPrice.setText(it.substring(0, it.length - 1))
binding.etSetPrice.setSelection(it.length - 1)
}
}
}
)
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.isPriceFreeLiveData.observe(this) {
if (it) {
viewModel.price = 0
binding.etSetPrice.setText("0")
binding.llSetPrice.visibility = View.GONE
binding.ivPriceFree.visibility = View.VISIBLE
binding.tvPriceFree.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_eeeeee
)
)
binding.llPriceFree.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.ivPricePaid.visibility = View.GONE
binding.tvPricePaid.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
)
)
binding.llPricePaid.setBackgroundResource(
R.drawable.bg_round_corner_6_7_1f1734_9970ff
)
} else {
binding.llSetPrice.visibility = View.VISIBLE
binding.ivPricePaid.visibility = View.VISIBLE
binding.tvPricePaid.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_eeeeee
)
)
binding.llPricePaid.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.ivPriceFree.visibility = View.GONE
binding.tvPriceFree.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
)
)
binding.llPriceFree.setBackgroundResource(
R.drawable.bg_round_corner_6_7_1f1734_9970ff
)
}
}
viewModel.isAvailableCommentLiveData.observe(this) {
if (it) {
binding.ivCommentYes.visibility = View.VISIBLE
binding.tvCommentYes.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_eeeeee
)
)
binding.llCommentYes.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.ivCommentNo.visibility = View.GONE
binding.tvCommentNo.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
)
)
binding.llCommentNo.setBackgroundResource(
R.drawable.bg_round_corner_6_7_1f1734_9970ff
)
} else {
binding.ivCommentNo.visibility = View.VISIBLE
binding.tvCommentNo.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_eeeeee
)
)
binding.llCommentNo.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.ivCommentYes.visibility = View.GONE
binding.tvCommentYes.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
)
)
binding.llCommentYes
.setBackgroundResource(R.drawable.bg_round_corner_6_7_1f1734_9970ff)
}
}
if (SharedPreferenceManager.isAuth) {
binding.llAgeAll.setOnClickListener {
viewModel.setAdult(false)
}
binding.llAge19.setOnClickListener {
viewModel.setAdult(true)
}
viewModel.isAdultLiveData.observe(this) {
if (it) {
binding.ivAgeAll.visibility = View.GONE
binding.llAgeAll.setBackgroundResource(R.drawable.bg_round_corner_6_7_1f1734)
binding.tvAgeAll.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
)
)
binding.ivAge19.visibility = View.VISIBLE
binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.tvAge19.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_eeeeee
)
)
} else {
binding.ivAge19.visibility = View.GONE
binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_1f1734)
binding.tvAge19.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_9970ff
)
)
binding.ivAgeAll.visibility = View.VISIBLE
binding.llAgeAll.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff)
binding.tvAgeAll.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_eeeeee
)
)
}
}
}
}
private fun getFileName(uri: Uri): String? {
val scheme = uri.scheme
var fileName: String? = null
if (scheme == "content") {
contentResolver.query(uri, null, null, null, null)?.use { cursor ->
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
if (nameIndex != -1 && cursor.moveToFirst()) {
fileName = cursor.getString(nameIndex)
}
}
} else if (scheme == "file") {
val file = File(uri.path ?: "")
fileName = file.name
}
return fileName
}
}

View File

@ -0,0 +1,221 @@
package kr.co.vividnext.sodalive.audio_content.upload
import android.net.Uri
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.google.gson.Gson
import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.audio_content.AudioContentRepository
import kr.co.vividnext.sodalive.audio_content.upload.theme.GetAudioContentThemeResponse
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import okio.BufferedSink
import java.io.File
class AudioContentUploadViewModel(
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 val _isAdultLiveData = MutableLiveData(false)
val isAdultLiveData: LiveData<Boolean>
get() = _isAdultLiveData
private val _isAvailableCommentLiveData = MutableLiveData(true)
val isAvailableCommentLiveData: LiveData<Boolean>
get() = _isAvailableCommentLiveData
private val _isPriceFreeLiveData = MutableLiveData(true)
val isPriceFreeLiveData: LiveData<Boolean>
get() = _isPriceFreeLiveData
lateinit var getRealPathFromURI: (Uri) -> String?
var title = ""
var detail = ""
var tags = ""
var price = 0
var theme: GetAudioContentThemeResponse? = null
var coverImageUri: Uri? = null
var contentUri: Uri? = null
fun setAdult(isAdult: Boolean) {
_isAdultLiveData.postValue(isAdult)
}
fun setAvailableComment(isAvailableComment: Boolean) {
_isAvailableCommentLiveData.postValue(isAvailableComment)
}
fun setPriceFree(isPriceFree: Boolean) {
_isPriceFreeLiveData.postValue(isPriceFree)
}
fun uploadAudioContent(onSuccess: () -> Unit) {
if (!_isLoading.value!! && validateData()) {
_isLoading.postValue(true)
val request = CreateAudioContentRequest(
title = title,
detail = detail,
tags = tags,
price = price,
themeId = theme!!.id,
isAdult = _isAdultLiveData.value!!,
isCommentAvailable = _isAvailableCommentLiveData.value!!
)
val requestJson = Gson().toJson(request)
val coverImage = if (coverImageUri != null) {
val file = File(getRealPathFromURI(coverImageUri!!))
MultipartBody.Part.createFormData(
"coverImage",
file.name,
body = object : RequestBody() {
override fun contentType(): MediaType {
return "image/*".toMediaType()
}
override fun writeTo(sink: BufferedSink) {
file.inputStream().use { inputStream ->
val buffer = ByteArray(1024)
var bytesRead: Int
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
sink.write(buffer, 0, bytesRead)
}
}
}
override fun contentLength(): Long {
return file.length()
}
}
)
} else {
null
}
val contentFile = if (contentUri != null) {
val file = File(getRealPathFromURI(contentUri!!))
MultipartBody.Part.createFormData(
"contentFile",
file.name,
body = object : RequestBody() {
override fun contentType(): MediaType {
return "audio/*".toMediaType()
}
override fun writeTo(sink: BufferedSink) {
file.inputStream().use { inputStream ->
val buffer = ByteArray(512)
var bytesRead: Int
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
sink.write(buffer, 0, bytesRead)
}
}
}
override fun contentLength(): Long {
return file.length()
}
}
)
} else {
null
}
if (coverImage == null) {
_toastLiveData.postValue("커버이미지를 선택해 주세요.")
return
}
if (contentFile == null) {
_toastLiveData.postValue("오디오 콘텐츠를 선택해 주세요.")
return
}
compositeDisposable.add(
repository.uploadAudioContent(
coverImage = coverImage,
contentFile = contentFile,
request = requestJson.toRequestBody("text/plain".toMediaType()),
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
onSuccess()
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
_isLoading.postValue(false)
},
{
_isLoading.postValue(false)
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
)
)
}
}
private fun validateData(): Boolean {
if (title.isBlank()) {
_toastLiveData.postValue("제목을 입력해 주세요.")
return false
}
if (detail.isBlank() || detail.length < 5) {
_toastLiveData.postValue("내용을 5자 이상 입력해 주세요.")
return false
}
if (theme == null) {
_toastLiveData.postValue("테마를 선택해 주세요.")
return false
}
if (coverImageUri == null) {
_toastLiveData.postValue("커버이미지를 선택해 주세요.")
return false
}
if (contentUri == null) {
_toastLiveData.postValue("오디오 콘텐츠를 선택해 주세요.")
return false
}
if (!isPriceFreeLiveData.value!! && price < 10) {
_toastLiveData.postValue("콘텐츠의 최소금액은 10코인 입니다.")
return false
}
return true
}
}

View File

@ -0,0 +1,13 @@
package kr.co.vividnext.sodalive.audio_content.upload
import com.google.gson.annotations.SerializedName
data class CreateAudioContentRequest(
@SerializedName("title") val title: String,
@SerializedName("detail") val detail: String,
@SerializedName("tags") val tags: String,
@SerializedName("price") val price: Int,
@SerializedName("themeId") val themeId: Long,
@SerializedName("isAdult") val isAdult: Boolean,
@SerializedName("isCommentAvailable") val isCommentAvailable: Boolean,
)

View File

@ -0,0 +1,64 @@
package kr.co.vividnext.sodalive.audio_content.upload.theme
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemAudioContentThemeBinding
class AudioContentThemeAdapter(
private val selectedTheme: GetAudioContentThemeResponse?,
private val onItemClick: (GetAudioContentThemeResponse) -> Unit
) : RecyclerView.Adapter<AudioContentThemeAdapter.ViewHolder>() {
inner class ViewHolder(
private val context: Context,
private val binding: ItemAudioContentThemeBinding
) : RecyclerView.ViewHolder(binding.root) {
private var isChecked = false
fun bind(item: GetAudioContentThemeResponse) {
if (selectedTheme == item) {
binding.ivThemeChecked.visibility = View.VISIBLE
binding.tvTheme.setTextColor(ContextCompat.getColor(context, R.color.color_9970ff))
isChecked = true
} else {
binding.ivThemeChecked.visibility = View.GONE
binding.tvTheme.setTextColor(ContextCompat.getColor(context, R.color.color_bbbbbb))
isChecked = false
}
binding.ivTheme.load(item.image) {
crossfade(true)
placeholder(R.drawable.ic_logo)
transformations(CircleCropTransformation())
}
binding.tvTheme.text = item.theme
binding.root.setOnClickListener { onItemClick(item) }
}
}
val items = mutableSetOf<GetAudioContentThemeResponse>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
parent.context,
ItemAudioContentThemeBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items.toList()[position])
}
override fun getItemCount() = items.size
}

View File

@ -0,0 +1,102 @@
package kr.co.vividnext.sodalive.audio_content.upload.theme
import android.annotation.SuppressLint
import android.app.Dialog
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.ImageView
import android.widget.Toast
import androidx.recyclerview.widget.GridLayoutManager
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.R
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
class AudioContentThemeFragment(
private val getSelectedTheme: () -> GetAudioContentThemeResponse?,
private val onItemClick: (GetAudioContentThemeResponse) -> Unit
) : BottomSheetDialogFragment() {
private val viewModel: AudioContentThemeViewModel by inject()
private lateinit var adapter: AudioContentThemeAdapter
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState)
dialog.setOnShowListener {
val d = it 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
}
}
return dialog
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View = inflater.inflate(R.layout.fragment_audio_content_theme, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
view.findViewById<ImageView>(R.id.iv_close).setOnClickListener {
dialog?.dismiss()
}
setupAdapter(view)
bindData()
viewModel.getThemes()
}
private fun setupAdapter(view: View) {
val recyclerView = view.findViewById<RecyclerView>(R.id.rv_themes)
adapter = AudioContentThemeAdapter(getSelectedTheme()) {
onItemClick(it)
dialog?.dismiss()
}
recyclerView.setHasFixedSize(true)
recyclerView.layoutManager = GridLayoutManager(requireContext(), 4)
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 = 13.3f.dpToPx().toInt()
outRect.bottom = 13.3f.dpToPx().toInt()
outRect.left = 13.3f.dpToPx().toInt()
outRect.right = 13.3f.dpToPx().toInt()
}
})
recyclerView.adapter = adapter
}
@SuppressLint("NotifyDataSetChanged")
private fun bindData() {
viewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireActivity(), it, Toast.LENGTH_LONG).show() }
}
viewModel.themeLiveData.observe(viewLifecycleOwner) {
adapter.items.addAll(it)
adapter.notifyDataSetChanged()
}
}
}

View File

@ -0,0 +1,47 @@
package kr.co.vividnext.sodalive.audio_content.upload.theme
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.audio_content.AudioContentRepository
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
class AudioContentThemeViewModel(private val repository: AudioContentRepository) : BaseViewModel() {
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
private val _themeLiveData = MutableLiveData<List<GetAudioContentThemeResponse>>()
val themeLiveData: LiveData<List<GetAudioContentThemeResponse>>
get() = _themeLiveData
fun getThemes() {
compositeDisposable.add(
repository.getAudioContentThemeList(token = "Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
_themeLiveData.postValue(it.data!!)
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
},
{
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
}

View File

@ -0,0 +1,9 @@
package kr.co.vividnext.sodalive.audio_content.upload.theme
import com.google.gson.annotations.SerializedName
data class GetAudioContentThemeResponse(
@SerializedName("id") val id: Long,
@SerializedName("theme") val theme: String,
@SerializedName("image") val image: String
)

View File

@ -0,0 +1,74 @@
package kr.co.vividnext.sodalive.base
import android.app.Activity
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.LayoutInflater
import android.view.View
import android.view.WindowManager
import androidx.appcompat.app.AlertDialog
import kr.co.vividnext.sodalive.databinding.DialogSodaBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
open class SodaDialog(
activity: Activity,
layoutInflater: LayoutInflater,
title: String,
desc: String,
confirmButtonTitle: String,
confirmButtonClick: () -> Unit,
cancelButtonTitle: String = "",
cancelButtonClick: (() -> Unit)? = null,
) {
private val alertDialog: AlertDialog
val dialogView = DialogSodaBinding.inflate(layoutInflater)
init {
val dialogBuilder = AlertDialog.Builder(activity)
dialogBuilder.setView(dialogView.root)
alertDialog = dialogBuilder.create()
alertDialog.setCancelable(false)
alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
dialogView.tvTitle.text = title
dialogView.tvDesc.text = desc
dialogView.tvCancel.text = cancelButtonTitle
dialogView.tvCancel.setOnClickListener {
alertDialog.dismiss()
cancelButtonClick?.let { it() }
}
dialogView.tvConfirm.text = confirmButtonTitle
dialogView.tvConfirm.setOnClickListener {
alertDialog.dismiss()
confirmButtonClick()
}
dialogView.tvCancel.visibility = if (cancelButtonTitle.isNotBlank()) {
View.VISIBLE
} else {
View.GONE
}
dialogView.tvConfirm.visibility = if (confirmButtonTitle.isNotBlank()) {
View.VISIBLE
} else {
View.GONE
}
}
fun show(width: Int) {
alertDialog.show()
val lp = WindowManager.LayoutParams()
lp.copyFrom(alertDialog.window?.attributes)
lp.width = width - (26.7f.dpToPx()).toInt()
lp.height = WindowManager.LayoutParams.WRAP_CONTENT
alertDialog.window?.attributes = lp
}
}

View File

@ -0,0 +1,9 @@
package kr.co.vividnext.sodalive.common
import retrofit2.Retrofit
class ApiBuilder {
fun <T> build(retrofit: Retrofit, service: Class<T>): T {
return retrofit.create(service)
}
}

View File

@ -0,0 +1,10 @@
package kr.co.vividnext.sodalive.common
import com.google.gson.annotations.SerializedName
data class ApiResponse<T>(
@SerializedName("success") val success: Boolean,
@SerializedName("data") val data: T? = null,
@SerializedName("message") val message: String? = null,
@SerializedName("errorProperty") val errorProperty: String? = null
)

View File

@ -0,0 +1,55 @@
package kr.co.vividnext.sodalive.common
object Constants {
const val PREF_CAN = "pref_can"
const val PREF_TOKEN = "pref_token"
const val PREF_EMAIL = "pref_email"
const val PREF_USER_ID = "pref_user_id"
const val PREF_IS_ADULT = "pref_is_adult"
const val PREF_NICKNAME = "pref_nickname"
const val PREF_USER_ROLE = "pref_user_role"
const val PREF_PUSH_TOKEN = "pref_push_token"
const val PREF_PROFILE_IMAGE = "pref_profile_image"
const val PREF_IS_CONTENT_PLAY_LOOP = "pref_is_content_play_loop"
const val PREF_IS_FOLLOWED_CREATOR_LIVE = "pref_is_followed_creator_live"
const val PREF_NOT_SHOWING_EVENT_POPUP_ID = "pref_not_showing_event_popup_id"
const val EXTRA_CAN = "extra_can"
const val EXTRA_DATA = "extra_data"
const val EXTRA_TERMS = "extra_terms"
const val EXTRA_EVENT = "extra_event"
const val EXTRA_NOTICE = "extra_notice"
const val EXTRA_ROOM_ID = "extra_room_id"
const val EXTRA_USER_ID = "extra_user_id"
const val EXTRA_NICKNAME = "extra_nickname"
const val EXTRA_MESSAGE_ID = "extra_message_id"
const val EXTRA_ROOM_DETAIL = "extra_room_detail"
const val EXTRA_MESSAGE_BOX = "extra_message_box"
const val EXTRA_TEXT_MESSAGE = "extra_text_message"
const val EXTRA_LIVE_TIME_NOW = "extra_live_time_now"
const val EXTRA_PREV_LIVE_ROOM = "extra_prev_live_room"
const val EXTRA_SELECT_RECIPIENT = "extra_select_recipient"
const val EXTRA_ROOM_CHANNEL_NAME = "extra_room_channel_name"
const val EXTRA_LIVE_RESERVATION_RESPONSE = "extra_live_reservation_response"
const val EXTRA_AUDIO_CONTENT_ID = "audio_content_id"
const val EXTRA_AUDIO_CONTENT_URL = "audio_content_url"
const val EXTRA_AUDIO_CONTENT_TITLE = "audio_content_title"
const val EXTRA_AUDIO_CONTENT_FREE = "audio_content_is_free"
const val EXTRA_AUDIO_CONTENT_PREVIEW = "audio_content_is_preview"
const val EXTRA_AUDIO_CONTENT_PLAYING = "audio_content_is_playing"
const val EXTRA_AUDIO_CONTENT_SHOWING = "audio_content_is_showing"
const val EXTRA_AUDIO_CONTENT_CHANGE_UI = "audio_content_change_ui"
const val EXTRA_AUDIO_CONTENT_PROGRESS = "audio_content_progress"
const val EXTRA_AUDIO_CONTENT_DURATION = "audio_content_duration"
const val EXTRA_AUDIO_CONTENT_COMMENT = "audio_content_comment"
const val EXTRA_AUDIO_CONTENT_LOADING = "audio_content_loading"
const val EXTRA_AUDIO_CONTENT_CREATOR_ID = "audio_content_creator_id"
const val EXTRA_AUDIO_CONTENT_NEXT_ACTION = "audio_content_next_action"
const val EXTRA_AUDIO_CONTENT_ALERT_PREVIEW = "audio_content_alert_preview"
const val EXTRA_AUDIO_CONTENT_COVER_IMAGE_URL = "audio_content_cover_image_url"
const val LIVE_SERVICE_NOTIFICATION_ID: Int = 2
const val ACTION_AUDIO_CONTENT_RECEIVER = "soda_live_action_content_receiver"
const val ACTION_MAIN_AUDIO_CONTENT_RECEIVER = "soda_live_action_main_content_receiver"
}

View File

@ -0,0 +1,15 @@
package kr.co.vividnext.sodalive.common
import android.graphics.Typeface
import android.text.TextPaint
import android.text.style.MetricAffectingSpan
class CustomTypefaceSpan(private val typeface: Typeface?) : MetricAffectingSpan() {
override fun updateDrawState(tp: TextPaint) {
tp.typeface = typeface
}
override fun updateMeasureState(textPaint: TextPaint) {
textPaint.typeface = typeface
}
}

View File

@ -0,0 +1,49 @@
package kr.co.vividnext.sodalive.common
import android.app.Activity
import android.graphics.Color
import android.graphics.drawable.AnimationDrawable
import android.graphics.drawable.ColorDrawable
import android.view.LayoutInflater
import android.view.WindowManager
import androidx.appcompat.app.AlertDialog
import kr.co.vividnext.sodalive.databinding.DialogLoadingBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
class LoadingDialog(
activity: Activity,
layoutInflater: LayoutInflater
) {
private val alertDialog: AlertDialog
private val dialogView = DialogLoadingBinding.inflate(layoutInflater)
private val animationDrawable: AnimationDrawable
init {
val dialogBuilder = AlertDialog.Builder(activity)
dialogBuilder.setView(dialogView.root)
animationDrawable = dialogView.tvLoading.compoundDrawables[1] as AnimationDrawable
alertDialog = dialogBuilder.create()
alertDialog.setCancelable(false)
alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
}
fun show(width: Int, message: String = "") {
alertDialog.show()
animationDrawable.start()
dialogView.tvLoading.text = message
val lp = WindowManager.LayoutParams()
lp.copyFrom(alertDialog.window?.attributes)
lp.width = width - (26.7f.dpToPx()).toInt()
lp.height = WindowManager.LayoutParams.WRAP_CONTENT
alertDialog.window?.attributes = lp
}
fun dismiss() {
animationDrawable.stop()
alertDialog.dismiss()
}
}

View File

@ -0,0 +1,14 @@
package kr.co.vividnext.sodalive.common
import android.content.Context
import io.objectbox.BoxStore
import kr.co.vividnext.sodalive.audio_content.MyObjectBox
import kr.co.vividnext.sodalive.audio_content.PlaybackTracking
class ObjectBox(context: Context) {
private var store: BoxStore = MyObjectBox.builder()
.androidContext(context.applicationContext)
.build()
val playbackTrackingBox = store.boxFor(PlaybackTracking::class.java)
}

View File

@ -0,0 +1,171 @@
package kr.co.vividnext.sodalive.common
import android.annotation.SuppressLint
import android.content.ContentUris
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.os.Environment
import android.provider.DocumentsContract
import android.provider.MediaStore
import android.text.TextUtils
object RealPathUtil {
fun getRealPath(context: Context, fileUri: Uri): String? {
return getRealPathFromURIAPI19(context, fileUri) // SDK > 19 (Android 4.4) and up
}
/**
* Get a file path from a Uri. This will get the the path for Storage Access
* Framework Documents, as well as the _data field for the MediaStore and
* other file-based ContentProviders.
*
* @param context The context.
* @param uri The Uri to query.
* @author Niks
*/
@SuppressLint("NewApi")
fun getRealPathFromURIAPI19(context: Context, uri: Uri): String? {
// DocumentProvider
if (DocumentsContract.isDocumentUri(context, uri)) {
// ExternalStorageProvider
if (isExternalStorageDocument(uri)) {
val docId = DocumentsContract.getDocumentId(uri)
val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
val type = split[0]
if ("primary".equals(type, ignoreCase = true)) {
return Environment.getExternalStorageDirectory().toString() + "/" + split[1]
}
} else if (isDownloadsDocument(uri)) {
var cursor: Cursor? = null
try {
cursor = context.contentResolver.query(
uri,
arrayOf(MediaStore.MediaColumns.DISPLAY_NAME),
null,
null,
null
)
cursor!!.moveToNext()
val fileName = cursor.getString(0)
val path = Environment.getExternalStorageDirectory()
.toString() + "/Download/" + fileName
if (!TextUtils.isEmpty(path)) {
return path
}
} finally {
cursor?.close()
}
val id = DocumentsContract.getDocumentId(uri)
if (id.startsWith("raw:")) {
return id.replaceFirst("raw:".toRegex(), "")
}
val contentUri = ContentUris.withAppendedId(
Uri.parse("content://downloads"),
java.lang.Long.valueOf(id)
)
return getDataColumn(context, contentUri, null, null)
} else if (isMediaDocument(uri)) {
val docId = DocumentsContract.getDocumentId(uri)
val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
val type = split[0]
var contentUri: Uri? = null
when (type) {
"image" -> contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
"video" -> contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
"audio" -> contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
}
val selection = "_id=?"
val selectionArgs = arrayOf(split[1])
return getDataColumn(context, contentUri, selection, selectionArgs)
} // MediaProvider
// DownloadsProvider
} else if ("content".equals(uri.scheme!!, ignoreCase = true)) {
// Return the remote address
return if (isGooglePhotosUri(uri)) uri.lastPathSegment else getDataColumn(
context,
uri,
null,
null
)
} else if ("file".equals(uri.scheme!!, ignoreCase = true)) {
return uri.path
} // File
// MediaStore (and general)
return null
}
/**
* Get the value of the data column for this Uri. This is useful for
* MediaStore Uris, and other file-based ContentProviders.
*
* @param context The context.
* @param uri The Uri to query.
* @param selection (Optional) Filter used in the query.
* @param selectionArgs (Optional) Selection arguments used in the query.
* @return The value of the _data column, which is typically a file path.
* @author Niks
*/
private fun getDataColumn(
context: Context,
uri: Uri?,
selection: String?,
selectionArgs: Array<String>?
): String? {
var cursor: Cursor? = null
val column = "_data"
val projection = arrayOf(column)
try {
cursor =
context.contentResolver.query(uri!!, projection, selection, selectionArgs, null)
if (cursor != null && cursor.moveToFirst()) {
val index = cursor.getColumnIndexOrThrow(column)
return cursor.getString(index)
}
} finally {
cursor?.close()
}
return null
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is ExternalStorageProvider.
*/
private fun isExternalStorageDocument(uri: Uri): Boolean {
return "com.android.externalstorage.documents" == uri.authority
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is DownloadsProvider.
*/
private fun isDownloadsDocument(uri: Uri): Boolean {
return "com.android.providers.downloads.documents" == uri.authority
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is MediaProvider.
*/
private fun isMediaDocument(uri: Uri): Boolean {
return "com.android.providers.media.documents" == uri.authority
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is Google Photos.
*/
private fun isGooglePhotosUri(uri: Uri): Boolean {
return "com.google.android.apps.photos.content" == uri.authority
}
}

View File

@ -0,0 +1,119 @@
package kr.co.vividnext.sodalive.common
import android.content.Context
import android.content.SharedPreferences
import androidx.preference.PreferenceManager
import kr.co.vividnext.sodalive.settings.notification.MemberRole
object SharedPreferenceManager {
private lateinit var sharedPreferences: SharedPreferences
fun init(context: Context) {
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
}
fun clear() {
sharedPreferences.edit { it.clear() }
}
private inline fun SharedPreferences.edit(operation: (SharedPreferences.Editor) -> Unit) {
val editor = this.edit()
operation(editor)
editor.apply()
}
private operator fun SharedPreferences.set(key: String, value: Any?) {
when (value) {
is String? -> edit { it.putString(key, value) }
is Int -> edit { it.putInt(key, value) }
is Boolean -> edit { it.putBoolean(key, value) }
is Float -> edit { it.putFloat(key, value) }
is Long -> edit { it.putLong(key, value) }
else -> throw UnsupportedOperationException("Error")
}
}
@Suppress("UNCHECKED_CAST")
private operator fun <T> SharedPreferences.get(key: String, defaultValue: T? = null): T {
return when (defaultValue) {
is String, null -> getString(key, defaultValue as? String) as T
is Int -> getInt(key, defaultValue as? Int ?: -1) as T
is Boolean -> getBoolean(key, defaultValue as? Boolean ?: false) as T
is Float -> getFloat(key, defaultValue as? Float ?: -1f) as T
is Long -> getLong(key, defaultValue as? Long ?: -1) as T
else -> throw UnsupportedOperationException("Error")
}
}
var token: String
get() = sharedPreferences[Constants.PREF_TOKEN, ""]
set(value) {
sharedPreferences[Constants.PREF_TOKEN] = value
}
var userId: Long
get() = sharedPreferences[Constants.PREF_USER_ID, 0]
set(value) {
sharedPreferences[Constants.PREF_USER_ID] = value
}
var nickname: String
get() = sharedPreferences[Constants.PREF_NICKNAME, ""]
set(value) {
sharedPreferences[Constants.PREF_NICKNAME] = value
}
var email: String
get() = sharedPreferences[Constants.PREF_EMAIL, ""]
set(value) {
sharedPreferences[Constants.PREF_EMAIL] = value
}
var profileImage: String
get() = sharedPreferences[Constants.PREF_PROFILE_IMAGE, ""]
set(value) {
sharedPreferences[Constants.PREF_PROFILE_IMAGE] = value
}
var can: Int
get() = sharedPreferences[Constants.PREF_CAN, 0]
set(value) {
sharedPreferences[Constants.PREF_CAN] = value
}
var role: String
get() = sharedPreferences[Constants.PREF_USER_ROLE, MemberRole.USER.name]
set(value) {
sharedPreferences[Constants.PREF_USER_ROLE] = value
}
var isAuth: Boolean
get() = sharedPreferences[Constants.PREF_IS_ADULT, false]
set(value) {
sharedPreferences[Constants.PREF_IS_ADULT] = value
}
var pushToken: String
get() = sharedPreferences[Constants.PREF_PUSH_TOKEN, ""]
set(value) {
sharedPreferences[Constants.PREF_PUSH_TOKEN] = value
}
var isFollowedCreatorLive: Boolean
get() = sharedPreferences[Constants.PREF_IS_FOLLOWED_CREATOR_LIVE, false]
set(value) {
sharedPreferences[Constants.PREF_IS_FOLLOWED_CREATOR_LIVE] = value
}
var isContentPlayLoop: Boolean
get() = sharedPreferences[Constants.PREF_IS_CONTENT_PLAY_LOOP, false]
set(value) {
sharedPreferences[Constants.PREF_IS_CONTENT_PLAY_LOOP] = value
}
var notShowingEventPopupId: Long
get() = sharedPreferences[Constants.PREF_NOT_SHOWING_EVENT_POPUP_ID, 0]
set(value) {
sharedPreferences[Constants.PREF_NOT_SHOWING_EVENT_POPUP_ID] = value
}
}

View File

@ -0,0 +1,90 @@
package kr.co.vividnext.sodalive.common
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.live.LiveViewModel
import kr.co.vividnext.sodalive.live.room.LiveRoomActivity
import org.koin.android.ext.android.inject
class SodaLiveService : Service() {
private val liveViewModel: LiveViewModel by inject()
var roomId: Long = 0
override fun onBind(intent: Intent?): IBinder? {
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val content = intent?.getStringExtra("content") ?: "라이브 진행중"
roomId = intent?.getLongExtra("roomId", 0) ?: 0L
updateNotification(content)
return START_STICKY
}
private fun updateNotification(content: String) {
startForeground(Constants.LIVE_SERVICE_NOTIFICATION_ID, createNotification(content))
}
private fun createNotification(content: String): Notification {
val notificationChannelId = "soda_live_service_foreground_service_channel"
val notificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel =
NotificationChannel(
notificationChannelId,
getString(R.string.app_name),
NotificationManager.IMPORTANCE_DEFAULT
)
notificationManager.createNotificationChannel(channel)
}
val intent = Intent(this, LiveRoomActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
val pendingIntent = PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
)
val notificationBuilder = NotificationCompat.Builder(this, notificationChannelId)
.setSmallIcon(R.drawable.ic_noti)
.setContentTitle(getString(R.string.app_name))
.setContentText(content)
.setOngoing(true)
.setSilent(true)
.setContentIntent(pendingIntent)
return notificationBuilder.build()
}
override fun onDestroy() {
liveViewModel.quitRoom(roomId) { }
super.onDestroy()
}
override fun onTaskRemoved(rootIntent: Intent?) {
stopSelf()
}
companion object {
fun stopService(context: Context) {
val intent = Intent(context, SodaLiveService::class.java)
context.stopService(intent)
}
}
}

View File

@ -0,0 +1,12 @@
package kr.co.vividnext.sodalive.common
object Utils {
fun convertDurationToString(duration: Int): String {
val durationSeconds = duration / 1000
val hours = (durationSeconds / 3600)
val minutes = ((durationSeconds % 3600) / 60)
val seconds = (durationSeconds % 60)
return "%02d:%02d:%02d".format(hours, minutes, seconds)
}
}

View File

@ -0,0 +1,217 @@
package kr.co.vividnext.sodalive.di
import android.content.Context
import com.google.gson.GsonBuilder
import kr.co.vividnext.sodalive.BuildConfig
import kr.co.vividnext.sodalive.audio_content.AudioContentApi
import kr.co.vividnext.sodalive.audio_content.AudioContentRepository
import kr.co.vividnext.sodalive.audio_content.AudioContentViewModel
import kr.co.vividnext.sodalive.audio_content.PlaybackTrackingRepository
import kr.co.vividnext.sodalive.audio_content.comment.AudioContentCommentListViewModel
import kr.co.vividnext.sodalive.audio_content.comment.AudioContentCommentReplyViewModel
import kr.co.vividnext.sodalive.audio_content.comment.AudioContentCommentRepository
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailViewModel
import kr.co.vividnext.sodalive.audio_content.main.AudioContentMainViewModel
import kr.co.vividnext.sodalive.audio_content.modify.AudioContentModifyViewModel
import kr.co.vividnext.sodalive.audio_content.order.AudioContentOrderListViewModel
import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadViewModel
import kr.co.vividnext.sodalive.audio_content.upload.theme.AudioContentThemeViewModel
import kr.co.vividnext.sodalive.common.ApiBuilder
import kr.co.vividnext.sodalive.common.ObjectBox
import kr.co.vividnext.sodalive.explorer.ExplorerApi
import kr.co.vividnext.sodalive.explorer.ExplorerRepository
import kr.co.vividnext.sodalive.explorer.ExplorerViewModel
import kr.co.vividnext.sodalive.explorer.profile.UserProfileViewModel
import kr.co.vividnext.sodalive.explorer.profile.follow.UserFollowerListViewModel
import kr.co.vividnext.sodalive.following.FollowingCreatorRepository
import kr.co.vividnext.sodalive.following.FollowingCreatorViewModel
import kr.co.vividnext.sodalive.live.LiveApi
import kr.co.vividnext.sodalive.live.LiveRepository
import kr.co.vividnext.sodalive.live.LiveViewModel
import kr.co.vividnext.sodalive.live.recommend.LiveRecommendApi
import kr.co.vividnext.sodalive.live.recommend.LiveRecommendRepository
import kr.co.vividnext.sodalive.live.reservation_status.LiveReservationStatusViewModel
import kr.co.vividnext.sodalive.live.room.LiveRoomViewModel
import kr.co.vividnext.sodalive.live.room.create.LiveRoomCreateViewModel
import kr.co.vividnext.sodalive.live.room.detail.LiveRoomDetailViewModel
import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationMessageViewModel
import kr.co.vividnext.sodalive.live.room.tag.LiveTagRepository
import kr.co.vividnext.sodalive.live.room.tag.LiveTagViewModel
import kr.co.vividnext.sodalive.live.room.update.LiveRoomEditViewModel
import kr.co.vividnext.sodalive.main.MainViewModel
import kr.co.vividnext.sodalive.message.MessageApi
import kr.co.vividnext.sodalive.message.MessageRepository
import kr.co.vividnext.sodalive.message.SelectMessageRecipientViewModel
import kr.co.vividnext.sodalive.message.text.TextMessageDetailViewModel
import kr.co.vividnext.sodalive.message.text.TextMessageViewModel
import kr.co.vividnext.sodalive.message.text.TextMessageWriteViewModel
import kr.co.vividnext.sodalive.message.voice.VoiceMessageViewModel
import kr.co.vividnext.sodalive.message.voice.VoiceMessageWriteViewModel
import kr.co.vividnext.sodalive.mypage.MyPageViewModel
import kr.co.vividnext.sodalive.mypage.auth.AuthApi
import kr.co.vividnext.sodalive.mypage.auth.AuthRepository
import kr.co.vividnext.sodalive.mypage.can.CanApi
import kr.co.vividnext.sodalive.mypage.can.CanRepository
import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeViewModel
import kr.co.vividnext.sodalive.mypage.can.payment.CanPaymentViewModel
import kr.co.vividnext.sodalive.mypage.can.status.CanStatusViewModel
import kr.co.vividnext.sodalive.mypage.service_center.FaqApi
import kr.co.vividnext.sodalive.mypage.service_center.FaqRepository
import kr.co.vividnext.sodalive.mypage.service_center.ServiceCenterViewModel
import kr.co.vividnext.sodalive.network.TokenAuthenticator
import kr.co.vividnext.sodalive.report.ReportApi
import kr.co.vividnext.sodalive.report.ReportRepository
import kr.co.vividnext.sodalive.settings.SettingsViewModel
import kr.co.vividnext.sodalive.settings.event.EventApi
import kr.co.vividnext.sodalive.settings.event.EventRepository
import kr.co.vividnext.sodalive.settings.event.EventViewModel
import kr.co.vividnext.sodalive.settings.notice.NoticeApi
import kr.co.vividnext.sodalive.settings.notice.NoticeRepository
import kr.co.vividnext.sodalive.settings.notice.NoticeViewModel
import kr.co.vividnext.sodalive.settings.notification.NotificationSettingsViewModel
import kr.co.vividnext.sodalive.settings.signout.SignOutViewModel
import kr.co.vividnext.sodalive.settings.terms.TermsApi
import kr.co.vividnext.sodalive.settings.terms.TermsRepository
import kr.co.vividnext.sodalive.settings.terms.TermsViewModel
import kr.co.vividnext.sodalive.user.UserApi
import kr.co.vividnext.sodalive.user.UserRepository
import kr.co.vividnext.sodalive.user.find_password.FindPasswordViewModel
import kr.co.vividnext.sodalive.user.login.LoginViewModel
import kr.co.vividnext.sodalive.user.signup.SignUpViewModel
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.context.startKoin
import org.koin.dsl.module
import retrofit2.Retrofit
import retrofit2.adapter.rxjava3.RxJava3CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
class AppDI(private val context: Context, isDebugMode: Boolean) {
private val baseUrl = BuildConfig.BASE_URL
private val otherModule = module {
single { GsonBuilder().create() }
single { ObjectBox(get()) }
}
private val networkModule = module {
single {
val logging = HttpLoggingInterceptor()
if (isDebugMode) {
logging.setLevel(HttpLoggingInterceptor.Level.BODY)
} else {
logging.setLevel(HttpLoggingInterceptor.Level.NONE)
}
OkHttpClient().newBuilder()
.addInterceptor(logging)
.authenticator(TokenAuthenticator(get()))
.build()
}
single {
Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
.client(get())
.build()
}
single { ApiBuilder().build(get(), CanApi::class.java) }
single { ApiBuilder().build(get(), AuthApi::class.java) }
single { ApiBuilder().build(get(), UserApi::class.java) }
single { ApiBuilder().build(get(), LiveApi::class.java) }
single { ApiBuilder().build(get(), TermsApi::class.java) }
single { ApiBuilder().build(get(), EventApi::class.java) }
single { ApiBuilder().build(get(), ReportApi::class.java) }
single { ApiBuilder().build(get(), LiveRecommendApi::class.java) }
single { ApiBuilder().build(get(), ExplorerApi::class.java) }
single { ApiBuilder().build(get(), MessageApi::class.java) }
single { ApiBuilder().build(get(), NoticeApi::class.java) }
single { ApiBuilder().build(get(), AudioContentApi::class.java) }
single { ApiBuilder().build(get(), FaqApi::class.java) }
}
private val viewModelModule = module {
viewModel { LoginViewModel(get()) }
viewModel { SignUpViewModel(get()) }
viewModel { TermsViewModel(get()) }
viewModel { FindPasswordViewModel(get()) }
viewModel { MainViewModel(get(), get(), get(), get()) }
viewModel { LiveViewModel(get(), get(), get()) }
viewModel { MyPageViewModel(get(), get()) }
viewModel { CanStatusViewModel(get()) }
viewModel { CanChargeViewModel(get()) }
viewModel { CanPaymentViewModel(get()) }
viewModel { LiveRoomDetailViewModel(get()) }
viewModel { LiveRoomCreateViewModel(get()) }
viewModel { LiveTagViewModel(get()) }
viewModel { LiveRoomEditViewModel(get()) }
viewModel { LiveRoomViewModel(get(), get(), get()) }
viewModel { LiveRoomDonationMessageViewModel(get()) }
viewModel { ExplorerViewModel(get()) }
viewModel { UserProfileViewModel(get(), get(), get()) }
viewModel { UserFollowerListViewModel(get(), get()) }
viewModel { TextMessageViewModel(get()) }
viewModel { TextMessageWriteViewModel(get()) }
viewModel { VoiceMessageViewModel(get()) }
viewModel { VoiceMessageWriteViewModel(get()) }
viewModel { SelectMessageRecipientViewModel(get(), get()) }
viewModel { SignOutViewModel(get()) }
viewModel { NoticeViewModel(get()) }
viewModel { EventViewModel(get()) }
viewModel { NotificationSettingsViewModel(get()) }
viewModel { SettingsViewModel(get()) }
viewModel { TextMessageDetailViewModel(get()) }
viewModel { LiveReservationStatusViewModel(get()) }
viewModel { AudioContentMainViewModel(get()) }
viewModel { AudioContentViewModel(get()) }
viewModel { AudioContentOrderListViewModel(get()) }
viewModel { AudioContentUploadViewModel(get()) }
viewModel { AudioContentModifyViewModel(get()) }
viewModel { AudioContentThemeViewModel(get()) }
viewModel { AudioContentDetailViewModel(get(), get(), get(), get()) }
viewModel { AudioContentCommentListViewModel(get()) }
viewModel { AudioContentCommentReplyViewModel(get()) }
viewModel { FollowingCreatorViewModel(get()) }
viewModel { ServiceCenterViewModel(get()) }
}
private val repositoryModule = module {
factory { UserRepository(get()) }
factory { TermsRepository(get()) }
factory { LiveRepository(get(), get()) }
factory { EventRepository(get()) }
factory { LiveRecommendRepository(get()) }
factory { AuthRepository(get()) }
factory { CanRepository(get()) }
factory { LiveTagRepository(get()) }
factory { ReportRepository(get()) }
factory { ExplorerRepository(get()) }
factory { MessageRepository(get()) }
factory { NoticeRepository(get()) }
factory { AudioContentRepository(get(), get()) }
factory { AudioContentCommentRepository(get()) }
factory { PlaybackTrackingRepository(get()) }
factory { FollowingCreatorRepository(get(), get()) }
factory { FaqRepository(get()) }
}
private val moduleList = listOf(
networkModule,
viewModelModule,
repositoryModule,
otherModule
)
init {
startKoin {
androidContext(context)
modules(moduleList)
}
}
}

View File

@ -0,0 +1,74 @@
package kr.co.vividnext.sodalive.dialog
import android.app.Activity
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.LayoutInflater
import android.view.View
import android.view.WindowManager
import androidx.appcompat.app.AlertDialog
import kr.co.vividnext.sodalive.databinding.DialogLiveBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
open class LiveDialog(
activity: Activity,
layoutInflater: LayoutInflater,
title: String,
desc: String,
confirmButtonTitle: String,
confirmButtonClick: () -> Unit,
cancelButtonTitle: String = "",
cancelButtonClick: (() -> Unit)? = null,
) {
private val alertDialog: AlertDialog
val dialogView = DialogLiveBinding.inflate(layoutInflater)
init {
val dialogBuilder = AlertDialog.Builder(activity)
dialogBuilder.setView(dialogView.root)
alertDialog = dialogBuilder.create()
alertDialog.setCancelable(false)
alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
dialogView.tvTitle.text = title
dialogView.tvDesc.text = desc
dialogView.tvCancel.text = cancelButtonTitle
dialogView.tvCancel.setOnClickListener {
alertDialog.dismiss()
cancelButtonClick?.let { it() }
}
dialogView.tvConfirm.text = confirmButtonTitle
dialogView.tvConfirm.setOnClickListener {
alertDialog.dismiss()
confirmButtonClick()
}
dialogView.tvCancel.visibility = if (cancelButtonTitle.isNotBlank()) {
View.VISIBLE
} else {
View.GONE
}
dialogView.tvConfirm.visibility = if (confirmButtonTitle.isNotBlank()) {
View.VISIBLE
} else {
View.GONE
}
}
fun show(width: Int) {
alertDialog.show()
val lp = WindowManager.LayoutParams()
lp.copyFrom(alertDialog.window?.attributes)
lp.width = width - (26.7f.dpToPx()).toInt()
lp.height = WindowManager.LayoutParams.WRAP_CONTENT
alertDialog.window?.attributes = lp
}
}

View File

@ -0,0 +1,121 @@
package kr.co.vividnext.sodalive.explorer
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Color
import android.graphics.Rect
import android.text.SpannableString
import android.text.Spanned
import android.text.style.ForegroundColorSpan
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.databinding.ItemExplorerBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
class ExplorerAdapter(
private val onClickItem: (Long) -> Unit
) : RecyclerView.Adapter<ExplorerAdapter.ViewHolder>() {
private val items = mutableListOf<GetExplorerSectionResponse>()
inner class ViewHolder(
private val context: Context,
private val binding: ItemExplorerBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetExplorerSectionResponse) {
setTitle(item)
setCreatorList(item)
}
private fun setTitle(item: GetExplorerSectionResponse) {
binding.tvTitle.text = if (
!item.coloredTitle.isNullOrBlank() &&
!item.color.isNullOrBlank()
) {
val spStr = SpannableString(item.title)
try {
spStr.setSpan(
ForegroundColorSpan(
Color.parseColor("#${item.color}")
),
item.title.indexOf(item.coloredTitle),
item.title.indexOf(item.coloredTitle) + item.coloredTitle.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
spStr
} catch (e: IllegalArgumentException) {
item.title
}
} else {
item.title
}
}
private fun setCreatorList(item: GetExplorerSectionResponse) {
val adapter = ExplorerSectionAdapter(onClickItem = onClickItem)
binding.rvExplorerSection.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
false
)
binding.rvExplorerSection.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.rvExplorerSection.adapter = adapter
adapter.addItems(item.creators)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
parent.context,
ItemExplorerBinding.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<GetExplorerSectionResponse>) {
this.items.addAll(items)
notifyDataSetChanged()
}
}

View File

@ -0,0 +1,63 @@
package kr.co.vividnext.sodalive.explorer
import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.explorer.profile.GetCreatorProfileResponse
import kr.co.vividnext.sodalive.explorer.profile.PostCreatorNoticeRequest
import kr.co.vividnext.sodalive.explorer.profile.cheers.PostWriteCheersRequest
import kr.co.vividnext.sodalive.explorer.profile.cheers.PutModifyCheersRequest
import kr.co.vividnext.sodalive.explorer.profile.follow.GetFollowerListResponse
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Path
import retrofit2.http.Query
interface ExplorerApi {
@GET("/explorer")
fun getExplorer(
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetExplorerResponse>>
@GET("/explorer/search/channel")
fun searchChannel(
@Query("channel") channel: String,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetRoomDetailUser>>>
@GET("/explorer/profile/{id}")
fun getCreatorProfile(
@Path("id") id: Long,
@Query("timezone") timezone: String,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetCreatorProfileResponse>>
@POST("/explorer/profile/cheers")
fun writeCheers(
@Body request: PostWriteCheersRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@PUT("/explorer/profile/cheers")
fun modifyCheers(
@Body request: PutModifyCheersRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@POST("/explorer/profile/notice")
fun writeCreatorNotice(
@Body request: PostCreatorNoticeRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@GET("/explorer/profile/{id}/follower-list")
fun getFollowerList(
@Path("id") userId: Long,
@Query("page") page: Int,
@Query("size") size: Int,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetFollowerListResponse>>
}

View File

@ -0,0 +1,202 @@
package kr.co.vividnext.sodalive.explorer
import android.annotation.SuppressLint
import android.app.Service
import android.content.Intent
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 android.widget.Toast
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.base.BaseFragment
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.FragmentExplorerBinding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.message.SelectMessageRecipientAdapter
import org.koin.android.ext.android.inject
import java.util.concurrent.TimeUnit
class ExplorerFragment : BaseFragment<FragmentExplorerBinding>(
FragmentExplorerBinding::inflate
) {
private val viewModel: ExplorerViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var adapter: ExplorerAdapter
private lateinit var imm: InputMethodManager
private val handler = Handler(Looper.getMainLooper())
private lateinit var searchChannelAdapter: SelectMessageRecipientAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
imm = requireContext().getSystemService(
Service.INPUT_METHOD_SERVICE
) as InputMethodManager
setupView()
bindData()
viewModel.getExplorer()
}
private fun hideKeyboard() {
handler.postDelayed({
imm.hideSoftInputFromWindow(
requireActivity().window.decorView.applicationWindowToken,
InputMethodManager.HIDE_NOT_ALWAYS
)
}, 100)
}
private fun setupView() {
adapter = ExplorerAdapter {
val intent = Intent(requireContext(), UserProfileActivity::class.java)
intent.putExtra(Constants.EXTRA_USER_ID, it)
startActivity(intent)
}
binding.rvExplorer.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.VERTICAL,
false
)
binding.rvExplorer.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 = 0
outRect.bottom = 30f.dpToPx().toInt()
}
adapter.itemCount - 1 -> {
outRect.top = 30f.dpToPx().toInt()
outRect.bottom = 0
}
else -> {
outRect.top = 30f.dpToPx().toInt()
outRect.bottom = 30f.dpToPx().toInt()
}
}
}
})
binding.rvExplorer.adapter = adapter
setupSearchChannelView()
}
private fun setupSearchChannelView() {
searchChannelAdapter = SelectMessageRecipientAdapter {
hideKeyboard()
val intent = Intent(requireContext(), UserProfileActivity::class.java)
intent.putExtra(Constants.EXTRA_USER_ID, it.id)
startActivity(intent)
}
binding.rvSearchChannel.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.VERTICAL,
false
)
binding.rvSearchChannel.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()
outRect.top = 13.3f.dpToPx().toInt()
outRect.bottom = 13.3f.dpToPx().toInt()
}
})
binding.rvSearchChannel.adapter = searchChannelAdapter
compositeDisposable.add(
binding.etSearchChannel.textChanges().skip(1)
.debounce(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe {
binding.ivX.visibility = if (it.length > 1) {
View.VISIBLE
} else {
View.GONE
}
if (it.length >= 2) {
viewModel.searchChannel(it.toString())
binding.rvSearchChannel.visibility = View.VISIBLE
binding.rvExplorer.visibility = View.GONE
} else {
binding.rvSearchChannel.visibility = View.GONE
binding.rvExplorer.visibility = View.VISIBLE
}
binding.tvResultX.visibility = View.GONE
}
)
binding.ivX.setOnClickListener {
hideKeyboard()
binding.etSearchChannel.setText("")
}
}
@SuppressLint("NotifyDataSetChanged")
private fun bindData() {
viewModel.isLoading.observe(viewLifecycleOwner) {
if (it) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
}
}
viewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
}
viewModel.responseLiveData.observe(viewLifecycleOwner) {
adapter.addItems(it.sections)
}
viewModel.searchChannelLiveData.observe(viewLifecycleOwner) {
searchChannelAdapter.items.clear()
if (it.isNotEmpty()) {
searchChannelAdapter.items.addAll(it)
binding.rvSearchChannel.visibility = View.VISIBLE
binding.tvResultX.visibility = View.GONE
} else {
binding.rvSearchChannel.visibility = View.GONE
binding.tvResultX.visibility = View.VISIBLE
}
searchChannelAdapter.notifyDataSetChanged()
}
}
}

View File

@ -0,0 +1,66 @@
package kr.co.vividnext.sodalive.explorer
import kr.co.vividnext.sodalive.explorer.profile.PostCreatorNoticeRequest
import kr.co.vividnext.sodalive.explorer.profile.cheers.PostWriteCheersRequest
import kr.co.vividnext.sodalive.explorer.profile.cheers.PutModifyCheersRequest
import java.util.TimeZone
class ExplorerRepository(
private val api: ExplorerApi
) {
fun getExplorer(token: String) = api.getExplorer(authHeader = token)
fun searchChannel(channel: String, token: String) = api.searchChannel(
channel = channel,
authHeader = token
)
fun getCreatorProfile(id: Long, token: String) = api.getCreatorProfile(
id = id,
timezone = TimeZone.getDefault().id,
authHeader = token
)
fun writeCheers(
parentCheersId: Long?,
creatorId: Long,
content: String,
token: String
) = api.writeCheers(
request = PostWriteCheersRequest(
parentId = parentCheersId,
creatorId = creatorId,
content = content
),
authHeader = token
)
fun modifyCheers(
cheersId: Long,
content: String,
token: String
) = api.modifyCheers(
request = PutModifyCheersRequest(
cheersId = cheersId,
content = content
),
authHeader = token
)
fun writeCreatorNotice(notice: String, token: String) = api.writeCreatorNotice(
request = PostCreatorNoticeRequest(notice),
authHeader = token
)
fun getFollowerList(
userId: Long,
page: Int,
size: Int,
token: String
) = api.getFollowerList(
userId = userId,
page = page - 1,
size = size,
authHeader = token
)
}

View File

@ -0,0 +1,54 @@
package kr.co.vividnext.sodalive.explorer
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.ItemExplorerSectionBinding
class ExplorerSectionAdapter(
private val onClickItem: (Long) -> Unit
) : RecyclerView.Adapter<ExplorerSectionAdapter.ViewHolder>() {
private val items = mutableListOf<GetExplorerSectionCreatorResponse>()
inner class ViewHolder(
private val binding: ItemExplorerSectionBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetExplorerSectionCreatorResponse) {
binding.tvNickname.text = item.nickname
binding.tvTags.text = item.tags
binding.ivProfile.load(item.profileImageUrl) {
transformations(CircleCropTransformation())
placeholder(R.drawable.ic_logo)
crossfade(true)
}
binding.root.setOnClickListener { onClickItem(item.id) }
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
ItemExplorerSectionBinding.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<GetExplorerSectionCreatorResponse>) {
this.items.addAll(items)
notifyDataSetChanged()
}
}

View File

@ -0,0 +1,91 @@
package kr.co.vividnext.sodalive.explorer
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.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser
class ExplorerViewModel(private val repository: ExplorerRepository) : BaseViewModel() {
private val _responseLiveData = MutableLiveData<GetExplorerResponse>()
val responseLiveData: LiveData<GetExplorerResponse>
get() = _responseLiveData
private val _searchChannelLiveData = MutableLiveData<List<GetRoomDetailUser>>()
val searchChannelLiveData: LiveData<List<GetRoomDetailUser>>
get() = _searchChannelLiveData
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
private var _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean>
get() = _isLoading
fun searchChannel(channel: String) {
compositeDisposable.add(
repository.searchChannel(
channel = channel,
token = "Bearer ${SharedPreferenceManager.token}"
).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
_searchChannelLiveData.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 getExplorer() {
if (!_isLoading.value!!) {
_isLoading.value = true
}
compositeDisposable.add(
repository.getExplorer(token = "Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
_responseLiveData.value = it.data!!
} 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("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
}

View File

@ -0,0 +1,21 @@
package kr.co.vividnext.sodalive.explorer
import com.google.gson.annotations.SerializedName
data class GetExplorerResponse(
@SerializedName("sections") val sections: List<GetExplorerSectionResponse>
)
data class GetExplorerSectionResponse(
@SerializedName("title") val title: String,
@SerializedName("coloredTitle") val coloredTitle: String?,
@SerializedName("color") val color: String?,
@SerializedName("creators") val creators: List<GetExplorerSectionCreatorResponse>
)
data class GetExplorerSectionCreatorResponse(
@SerializedName("id") val id: Long,
@SerializedName("nickname") val nickname: String,
@SerializedName("tags") val tags: String,
@SerializedName("profileImageUrl") val profileImageUrl: String
)

View File

@ -0,0 +1,73 @@
package kr.co.vividnext.sodalive.explorer.profile
import android.content.Intent
import android.widget.Toast
import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ActivityCreatorNoticeWriteBinding
import kr.co.vividnext.sodalive.explorer.ExplorerRepository
import org.koin.android.ext.android.inject
class CreatorNoticeWriteActivity : BaseActivity<ActivityCreatorNoticeWriteBinding>(
ActivityCreatorNoticeWriteBinding::inflate
) {
private val repository: ExplorerRepository by inject()
private lateinit var loadingDialog: LoadingDialog
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.text = "공지사항 쓰기"
binding.toolbar.tvBack.setOnClickListener { finish() }
val notice = intent.getStringExtra("notice")
binding.etContent.setText(notice)
binding.tvSave.setOnClickListener {
loadingDialog.show(screenWidth)
val writtenNotice = binding.etContent.text.toString()
compositeDisposable.add(
repository.writeCreatorNotice(
notice = writtenNotice,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
loadingDialog.dismiss()
val message = it.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
Toast.makeText(
applicationContext,
message,
Toast.LENGTH_LONG
).show()
if (it.success) {
val dataIntent = Intent()
dataIntent.putExtra("notice", writtenNotice)
setResult(RESULT_OK, dataIntent)
finish()
}
},
{
loadingDialog.dismiss()
it.message?.let { message -> Logger.e(message) }
Toast.makeText(
applicationContext,
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.",
Toast.LENGTH_LONG
).show()
}
)
)
}
}
}

View File

@ -0,0 +1,17 @@
package kr.co.vividnext.sodalive.explorer.profile
import com.google.gson.annotations.SerializedName
data class GetCheersResponse(
@SerializedName("totalCount") val totalCount: Int,
@SerializedName("cheers") val cheers: List<GetCheersResponseItem>
)
data class GetCheersResponseItem(
@SerializedName("cheersId") val cheersId: Long,
@SerializedName("nickname") val nickname: String,
@SerializedName("profileUrl") val profileUrl: String,
@SerializedName("content") val content: String,
@SerializedName("date") val date: String,
@SerializedName("replyList") val replyList: List<GetCheersResponseItem>
)

View File

@ -0,0 +1,95 @@
package kr.co.vividnext.sodalive.explorer.profile
import com.google.gson.annotations.SerializedName
data class GetCreatorProfileResponse(
@SerializedName("creator")
val creator: CreatorResponse,
@SerializedName("userDonationRanking")
val userDonationRanking: List<UserDonationRankingResponse>,
@SerializedName("similarCreatorList")
val similarCreatorList: List<SimilarCreatorResponse>,
@SerializedName("liveRoomList")
val liveRoomList: List<LiveRoomResponse>,
@SerializedName("audioContentList")
val audioContentList: List<GetAudioContentListItem>,
@SerializedName("notice")
val notice: String,
@SerializedName("cheers")
val cheers: GetCheersResponse,
@SerializedName("activitySummary")
val activitySummary: GetCreatorActivitySummary,
@SerializedName("isBlock")
val isBlock: Boolean
)
data class CreatorResponse(
@SerializedName("creatorId") val creatorId: Long,
@SerializedName("profileUrl") val profileUrl: String,
@SerializedName("nickname") val nickname: String,
@SerializedName("tags") val tags: List<String>,
@SerializedName("introduce") val introduce: String = "",
@SerializedName("instagramUrl") val instagramUrl: String? = null,
@SerializedName("youtubeUrl") val youtubeUrl: String? = null,
@SerializedName("websiteUrl") val websiteUrl: String? = null,
@SerializedName("blogUrl") val blogUrl: String? = null,
@SerializedName("isAvailableChat") val isAvailableChat: Boolean = true,
@SerializedName("isNotification") val isNotification: Boolean,
@SerializedName("notificationRecipientCount") val notificationRecipientCount: Int
)
data class UserDonationRankingResponse(
@SerializedName("userId") val userId: Long,
@SerializedName("nickname") val nickname: String,
@SerializedName("profileImage") val profileImage: String,
@SerializedName("donationCoin") val donationCoin: Int
)
data class SimilarCreatorResponse(
@SerializedName("userId") val userId: Long,
@SerializedName("nickname") val nickname: String,
@SerializedName("profileImage") val profileImage: String,
@SerializedName("tags") val tags: List<String>
)
data class LiveRoomResponse(
@SerializedName("roomId") val roomId: Long,
@SerializedName("title") val title: String,
@SerializedName("content") val content: String,
@SerializedName("isPaid") val isPaid: Boolean,
@SerializedName("beginDateTime") val beginDateTime: String,
@SerializedName("coverImageUrl") val coverImageUrl: String,
@SerializedName("isAdult") val isAdult: Boolean,
@SerializedName("price") val price: Int,
@SerializedName("channelName") val channelName: String?,
@SerializedName("managerNickname") val managerNickname: String,
@SerializedName("isReservation") val isReservation: Boolean,
@SerializedName("isActive") val isActive: Boolean,
@SerializedName("isPrivateRoom") val isPrivateRoom: Boolean
)
data class GetAudioContentListResponse(
@SerializedName("totalCount") val totalCount: Int,
@SerializedName("items") val items: List<GetAudioContentListItem>
)
data class GetAudioContentListItem(
@SerializedName("contentId") val contentId: Long,
@SerializedName("coverImageUrl") val coverImageUrl: String,
@SerializedName("title") val title: String,
@SerializedName("price") val price: Int,
@SerializedName("themeStr") val themeStr: String,
@SerializedName("duration") val duration: String?,
@SerializedName("likeCount") val likeCount: Int,
@SerializedName("commentCount") val commentCount: Int,
@SerializedName("isAdult") val isAdult: Boolean
)
data class GetCreatorActivitySummary(
@SerializedName("liveCount") val liveCount: Int,
@SerializedName("liveTime") val liveTime: Int,
@SerializedName("liveContributorCount") val liveContributorCount: Int,
@SerializedName("contentCount") val contentCount: Int
)

View File

@ -0,0 +1,5 @@
package kr.co.vividnext.sodalive.explorer.profile
import com.google.gson.annotations.SerializedName
data class MemberBlockRequest(@SerializedName("blockMemberId") val blockMemberId: Long)

View File

@ -0,0 +1,8 @@
package kr.co.vividnext.sodalive.explorer.profile
import com.google.gson.annotations.SerializedName
data class PostCreatorNoticeRequest(
@SerializedName("notice")
val notice: String
)

View File

@ -0,0 +1,773 @@
package kr.co.vividnext.sodalive.explorer.profile
import android.annotation.SuppressLint
import android.app.Activity
import android.app.AlertDialog
import android.app.Service
import android.content.Context
import android.content.Intent
import android.graphics.Rect
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.webkit.URLUtil
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.widget.PopupMenu
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
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.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ActivityUserProfileBinding
import kr.co.vividnext.sodalive.explorer.profile.cheers.UserProfileCheersAdapter
import kr.co.vividnext.sodalive.explorer.profile.donation.UserProfileDonationAdapter
import kr.co.vividnext.sodalive.explorer.profile.donation.UserProfileDonationAllViewActivity
import kr.co.vividnext.sodalive.explorer.profile.fantalk.UserProfileFantalkAllViewActivity
import kr.co.vividnext.sodalive.explorer.profile.follow.UserFollowerListActivity
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.moneyFormat
import kr.co.vividnext.sodalive.live.LiveViewModel
import kr.co.vividnext.sodalive.live.reservation.complete.LiveReservationCompleteActivity
import kr.co.vividnext.sodalive.live.room.LiveRoomActivity
import kr.co.vividnext.sodalive.live.room.dialog.LivePaymentDialog
import kr.co.vividnext.sodalive.live.room.dialog.LiveRoomPasswordDialog
import kr.co.vividnext.sodalive.report.CheersReportDialog
import kr.co.vividnext.sodalive.report.ProfileReportDialog
import kr.co.vividnext.sodalive.report.ReportType
import kr.co.vividnext.sodalive.report.UserReportDialog
import org.koin.android.ext.android.inject
class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
ActivityUserProfileBinding::inflate
) {
private val viewModel: UserProfileViewModel by inject()
private val liveViewModel: LiveViewModel by inject()
private lateinit var imm: InputMethodManager
private lateinit var loadingDialog: LoadingDialog
private lateinit var liveAdapter: UserProfileLiveAdapter
private lateinit var donationAdapter: UserProfileDonationAdapter
private lateinit var similarCreatorAdapter: UserProfileSimilarCreatorAdapter
private lateinit var cheersAdapter: UserProfileCheersAdapter
private lateinit var noticeWriteLauncher: ActivityResultLauncher<Intent>
private val handler = Handler(Looper.getMainLooper())
private var userId: Long = 0
override fun onCreate(savedInstanceState: Bundle?) {
userId = intent.getLongExtra(Constants.EXTRA_USER_ID, 0)
super.onCreate(savedInstanceState)
imm = getSystemService(Service.INPUT_METHOD_SERVICE) as InputMethodManager
noticeWriteLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode == Activity.RESULT_OK) {
val writtenNotice = it.data?.getStringExtra("notice")
binding.tvNotice.text = writtenNotice?.ifBlank {
"공지사항이 없습니다."
}
}
}
if (userId <= 0) {
Toast.makeText(applicationContext, "잘못된 요청입니다.", Toast.LENGTH_LONG).show()
finish()
}
bindData()
}
override fun onResume() {
super.onResume()
viewModel.getCreatorProfile(userId) { finish() }
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.tvBack.text = "채널"
binding.tvBack.setOnClickListener { finish() }
binding.ivMenu.setOnClickListener {
showOptionMenu(
this,
binding.ivMenu,
)
}
binding.layoutUserProfile.ivShare.setOnClickListener {
viewModel.shareChannel(userId = userId) {
val intent = Intent(Intent.ACTION_SEND)
intent.type = "text/plain"
intent.putExtra(Intent.EXTRA_TEXT, it)
val shareIntent = Intent.createChooser(intent, "채널 공유")
startActivity(shareIntent)
}
}
setupLiveView()
setupDonationView()
setupSimilarCreatorView()
setupFanTalkView()
}
private fun hideKeyboard(onAfterExecute: () -> Unit) {
handler.postDelayed({
imm.hideSoftInputFromWindow(
window.decorView.applicationWindowToken,
InputMethodManager.HIDE_NOT_ALWAYS
)
onAfterExecute()
}, 100)
}
private fun showOptionMenu(context: Context, v: View) {
val popup = PopupMenu(context, v)
val inflater = popup.menuInflater
if (viewModel.creatorProfileLiveData.value!!.isBlock) {
inflater.inflate(R.menu.user_profile_option_menu_2, popup.menu)
popup.setOnMenuItemClickListener {
when (it.itemId) {
R.id.menu_user_block -> {
viewModel.userUnBlock(userId)
}
R.id.menu_user_report -> {
showUserReportDialog()
}
R.id.menu_profile_report -> {
showProfileReportDialog()
}
}
true
}
} else {
inflater.inflate(R.menu.user_profile_option_menu, popup.menu)
popup.setOnMenuItemClickListener {
when (it.itemId) {
R.id.menu_user_block -> {
showUserBlockDialog()
}
R.id.menu_user_report -> {
showUserReportDialog()
}
R.id.menu_profile_report -> {
showProfileReportDialog()
}
}
true
}
}
popup.show()
}
private fun showUserBlockDialog() {
val dialog = AlertDialog.Builder(this)
dialog.setTitle("사용자 차단")
dialog.setMessage(
"${binding.layoutUserProfile.tvNickname.text}님을 차단하시겠습니까?\n\n" +
"사용자를 차단하면 사용자는 아래 기능이 제한됩니다.\n" +
"- 내가 개설한 라이브 입장 불가\n" +
"- 나에게 메시지 보내기 불가\n" +
"- 내 채널의 팬Talk 작성불가"
)
dialog.setPositiveButton("차단") { _, _ ->
viewModel.userBlock(userId)
}
dialog.setNegativeButton("취소") { _, _ -> }
dialog.show()
}
private fun showUserReportDialog() {
val dialog = UserReportDialog(this, layoutInflater) {
viewModel.report(
type = ReportType.USER,
userId = userId,
reason = it
)
}
dialog.show(screenWidth)
}
private fun showProfileReportDialog() {
val dialog = ProfileReportDialog(this, layoutInflater) {
viewModel.report(
type = ReportType.PROFILE,
userId = userId
)
}
dialog.show(screenWidth)
}
private fun setupLiveView() {
val recyclerView = binding.layoutUserProfileLive.rvLive
liveAdapter = UserProfileLiveAdapter(
onClickParticipant = { enterLiveRoom(roomId = it.roomId) },
onClickReservation = { reservationRoom(roomId = it.roomId) }
)
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)
when (parent.getChildAdapterPosition(view)) {
liveAdapter.itemCount - 1 -> {
outRect.bottom = 0
}
else -> {
outRect.bottom = 13.3f.dpToPx().toInt()
}
}
}
})
recyclerView.adapter = liveAdapter
}
private fun setupDonationView() {
binding.layoutUserProfileDonation.tvAll.setOnClickListener {
val intent = Intent(applicationContext, UserProfileDonationAllViewActivity::class.java)
intent.putExtra(Constants.EXTRA_USER_ID, userId)
startActivity(intent)
}
val recyclerView = binding.layoutUserProfileDonation.rvDonation
donationAdapter = UserProfileDonationAdapter()
recyclerView.layoutManager = LinearLayoutManager(
applicationContext,
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()
}
donationAdapter.itemCount - 1 -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 0
}
else -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 6.7f.dpToPx().toInt()
}
}
}
})
recyclerView.adapter = donationAdapter
}
private fun setupSimilarCreatorView() {
val recyclerView = binding.layoutUserProfileSimilarCreator.rvSimilarCreator
similarCreatorAdapter = UserProfileSimilarCreatorAdapter {
val intent = Intent(applicationContext, UserProfileActivity::class.java)
intent.putExtra(Constants.EXTRA_USER_ID, it.userId)
startActivity(intent)
}
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)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.top = 0
outRect.bottom = 10f.dpToPx().toInt()
}
similarCreatorAdapter.itemCount - 1 -> {
outRect.top = 10f.dpToPx().toInt()
outRect.bottom = 0
}
else -> {
outRect.top = 10f.dpToPx().toInt()
outRect.bottom = 10f.dpToPx().toInt()
}
}
}
})
recyclerView.adapter = similarCreatorAdapter
}
private fun setupFanTalkView() {
binding.layoutUserProfileFanTalk.tvAll.setOnClickListener {
val intent = Intent(
applicationContext,
UserProfileFantalkAllViewActivity::class.java
)
intent.putExtra(Constants.EXTRA_USER_ID, userId)
startActivity(intent)
}
setupCheersView()
}
private fun setupCheersView() {
binding.layoutUserProfileFanTalk.ivSend.setOnClickListener {
hideKeyboard {
viewModel.writeCheers(
creatorId = userId,
cheersContent = binding.layoutUserProfileFanTalk.etCheer.text.toString()
)
}
}
val rvCheers = binding.layoutUserProfileFanTalk.rvCheers
cheersAdapter = UserProfileCheersAdapter(
userId = userId,
enterReply = { cheersId, content ->
hideKeyboard {
viewModel.writeCheers(
parentCheersId = cheersId,
creatorId = userId,
cheersContent = content
)
}
},
modifyReply = { cheersId, content ->
hideKeyboard {
viewModel.modifyCheers(
cheersId = cheersId,
creatorId = userId,
cheersContent = content
)
}
},
onClickReport = { showCheersReportPopup(it) }
)
rvCheers.layoutManager = LinearLayoutManager(
applicationContext,
LinearLayoutManager.VERTICAL,
false
)
rvCheers.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
outRect.bottom = 0
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.top = 0
}
cheersAdapter.itemCount - 1 -> {
outRect.top = 10.dpToPx().toInt()
outRect.bottom = 10.dpToPx().toInt()
}
else -> {
outRect.top = 10.dpToPx().toInt()
}
}
}
})
rvCheers.adapter = cheersAdapter
}
private fun showCheersReportPopup(cheersId: Long) {
val dialog = CheersReportDialog(this, layoutInflater) {
if (it.isBlank()) {
Toast.makeText(
applicationContext,
"신고 이유를 선택해 주세요.",
Toast.LENGTH_LONG
).show()
} else {
viewModel.cheersReport(cheersId, reason = it)
}
}
dialog.show(screenWidth)
}
private fun bindData() {
liveViewModel.toastLiveData.observe(this) {
it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() }
}
liveViewModel.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.isLoading.observe(this) {
if (it) {
loadingDialog.show(screenWidth, "")
} else {
loadingDialog.dismiss()
}
}
viewModel.creatorProfileLiveData.observe(this) {
setCheers(it.cheers)
setCreatorProfile(it.creator)
setCreatorNotice(it.notice, it.creator.creatorId)
setLiveRoomList(it.liveRoomList)
setSimilarCreatorList(it.similarCreatorList)
setUserDonationRanking(it.userDonationRanking)
setActivitySummary(it.activitySummary)
}
viewModel.isExpandNotice.observe(this) {
if (it) {
binding.tvNotice.maxLines = Int.MAX_VALUE
} else {
binding.tvNotice.maxLines = 1
}
}
}
private fun setActivitySummary(activitySummary: GetCreatorActivitySummary) {
binding.tvLiveCount.text = activitySummary.liveCount.moneyFormat()
binding.tvLiveContributorCount.text = activitySummary.liveContributorCount.moneyFormat()
binding.tvLiveTime.text = activitySummary.liveTime.moneyFormat()
binding.tvContentCount.text = activitySummary.contentCount.moneyFormat()
}
@SuppressLint("NotifyDataSetChanged")
private fun setCheers(cheers: GetCheersResponse) {
binding.layoutUserProfileFanTalk.etCheer.setText("")
cheersAdapter.items.clear()
binding.layoutUserProfileFanTalk.tvCheersCount.text = cheers.totalCount.toString()
cheersAdapter.items.addAll(cheers.cheers)
cheersAdapter.notifyDataSetChanged()
if (cheersAdapter.itemCount <= 0) {
binding.layoutUserProfileFanTalk.rvCheers.visibility = View.GONE
binding.layoutUserProfileFanTalk.tvNoCheers.visibility = View.VISIBLE
} else {
binding.layoutUserProfileFanTalk.rvCheers.visibility = View.VISIBLE
binding.layoutUserProfileFanTalk.tvNoCheers.visibility = View.GONE
}
}
@SuppressLint("SetTextI18n")
private fun setCreatorProfile(creator: CreatorResponse) {
val layoutUserProfile = binding.layoutUserProfile
if (creator.creatorId == SharedPreferenceManager.userId) {
layoutUserProfile.tvFollowerList.visibility = View.VISIBLE
layoutUserProfile.llNotification.visibility = View.GONE
layoutUserProfile.tvFollowerList.setOnClickListener {
val intent = Intent(applicationContext, UserFollowerListActivity::class.java)
intent.putExtra(Constants.EXTRA_USER_ID, creator.creatorId)
startActivity(intent)
}
} else {
layoutUserProfile.llNotification.visibility = View.VISIBLE
layoutUserProfile.tvFollowerList.visibility = View.GONE
}
layoutUserProfile.ivProfile.load(creator.profileUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
transformations(CircleCropTransformation())
}
binding.tvBack.text = "${creator.nickname}님의 채널"
layoutUserProfile.tvNickname.text = creator.nickname
layoutUserProfile.tvTags.text = creator.tags.joinToString(" ") { "#$it" }
if (creator.websiteUrl.isNullOrBlank() || !URLUtil.isValidUrl(creator.websiteUrl)) {
layoutUserProfile.ivWebsite.visibility = View.GONE
} else {
layoutUserProfile.ivWebsite.visibility = View.VISIBLE
layoutUserProfile.ivWebsite.setOnClickListener {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(creator.websiteUrl)))
}
}
if (creator.blogUrl.isNullOrBlank() || !URLUtil.isValidUrl(creator.blogUrl)) {
layoutUserProfile.ivBlog.visibility = View.GONE
} else {
layoutUserProfile.ivBlog.visibility = View.VISIBLE
layoutUserProfile.ivBlog.setOnClickListener {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(creator.blogUrl)))
}
}
if (creator.instagramUrl.isNullOrBlank() || !URLUtil.isValidUrl(creator.instagramUrl)) {
layoutUserProfile.ivInstagram.visibility = View.GONE
} else {
layoutUserProfile.ivInstagram.visibility = View.VISIBLE
layoutUserProfile.ivInstagram.setOnClickListener {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(creator.instagramUrl)))
}
}
if (creator.youtubeUrl.isNullOrBlank() || !URLUtil.isValidUrl(creator.youtubeUrl)) {
layoutUserProfile.ivYoutube.visibility = View.GONE
} else {
layoutUserProfile.ivYoutube.visibility = View.VISIBLE
layoutUserProfile.ivYoutube.setOnClickListener {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(creator.youtubeUrl)))
}
}
if (creator.isNotification) {
layoutUserProfile.ivNotification.setImageResource(R.drawable.btn_notification_selected)
layoutUserProfile.ivNotification.setOnClickListener {
viewModel.unFollow(creator.creatorId)
}
} else {
layoutUserProfile.ivNotification.setImageResource(R.drawable.btn_notification)
layoutUserProfile.ivNotification.setOnClickListener {
viewModel.follow(creator.creatorId)
}
}
layoutUserProfile
.tvNotificationCount
.text = "팔로워 ${creator.notificationRecipientCount.moneyFormat()}"
val introduce = creator.introduce.ifBlank {
"채널 소개내용이 없습니다."
}
binding.layoutUserProfileIntroduce.tvIntroduce.text = introduce
}
private fun setCreatorNotice(notice: String, creatorId: Long) {
binding.tvNotice.text = notice.ifBlank {
"공지사항이 없습니다."
}
binding.rlNotice.setOnClickListener {
if (creatorId == SharedPreferenceManager.userId) {
val intent = Intent(applicationContext, CreatorNoticeWriteActivity::class.java)
intent.putExtra("notice", notice)
noticeWriteLauncher.launch(intent)
} else {
viewModel.toggleExpandNotice()
}
}
binding.ivWrite.visibility = if (creatorId == SharedPreferenceManager.userId) {
View.VISIBLE
} else {
View.GONE
}
}
@SuppressLint("NotifyDataSetChanged")
private fun setLiveRoomList(liveRoomList: List<LiveRoomResponse>) {
if (liveRoomList.isEmpty()) {
binding.layoutUserProfileLive.root.visibility = View.GONE
} else {
binding.layoutUserProfileLive.root.visibility = View.VISIBLE
liveAdapter.items.clear()
liveAdapter.items.addAll(liveRoomList)
liveAdapter.notifyDataSetChanged()
}
}
@SuppressLint("NotifyDataSetChanged")
private fun setSimilarCreatorList(similarCreatorList: List<SimilarCreatorResponse>) {
if (similarCreatorList.isEmpty()) {
binding.llUserProfileSimilarCreator.visibility = View.GONE
} else {
binding.llUserProfileSimilarCreator.visibility = View.VISIBLE
similarCreatorAdapter.items.clear()
similarCreatorAdapter.items.addAll(similarCreatorList)
similarCreatorAdapter.notifyDataSetChanged()
}
}
@SuppressLint("NotifyDataSetChanged")
private fun setUserDonationRanking(userDonationRanking: List<UserDonationRankingResponse>) {
if (userDonationRanking.isEmpty()) {
binding.llUserProfileDonation.visibility = View.GONE
} else {
binding.llUserProfileDonation.visibility = View.VISIBLE
donationAdapter.items.clear()
donationAdapter.items.addAll(userDonationRanking)
donationAdapter.notifyDataSetChanged()
}
}
private fun reservationRoom(roomId: Long) {
liveViewModel.getRoomDetail(roomId) {
if (it.manager.id == SharedPreferenceManager.userId) {
showToast("내가 만든 라이브는 예약할 수 없습니다.")
} else {
if (it.isPrivateRoom) {
LiveRoomPasswordDialog(
activity = this,
layoutInflater = layoutInflater,
can = if (it.isPaid) 0 else it.price,
confirmButtonClick = { password ->
handler.postDelayed({
processLiveReservation(roomId, password)
}, 300)
}
).show(screenWidth)
} else {
if (it.price == 0 || it.isPaid) {
processLiveReservation(roomId)
} else {
LivePaymentDialog(
activity = this,
layoutInflater = layoutInflater,
title = "${it.price.moneyFormat()}캔으로 예약",
desc = "'${it.title}' 라이브에 참여하기 위해 결제합니다.",
confirmButtonTitle = "예약하기",
confirmButtonClick = { processLiveReservation(roomId) },
cancelButtonTitle = "취소",
cancelButtonClick = {}
).show(screenWidth)
}
}
}
}
}
private fun processLiveReservation(roomId: Long, password: String? = null) {
liveViewModel.reservationRoom(roomId, password) {
val intent = Intent(
applicationContext,
LiveReservationCompleteActivity::class.java
)
intent.putExtra(Constants.EXTRA_LIVE_RESERVATION_RESPONSE, it)
startActivity(intent)
}
}
private fun enterLiveRoom(roomId: Long) {
val onEnterRoomSuccess = {
runOnUiThread {
val intent = Intent(applicationContext, LiveRoomActivity::class.java)
intent.putExtra(Constants.EXTRA_ROOM_ID, roomId)
startActivity(intent)
}
}
liveViewModel.getRoomDetail(roomId) {
if (it.channelName != null) {
if (it.manager.id == SharedPreferenceManager.userId) {
liveViewModel.enterRoom(roomId, onEnterRoomSuccess)
} else if (it.price == 0 || it.isPaid) {
if (it.isPrivateRoom) {
LiveRoomPasswordDialog(
activity = this,
layoutInflater = layoutInflater,
can = 0,
confirmButtonClick = { password ->
liveViewModel.enterRoom(
roomId = roomId,
onSuccess = onEnterRoomSuccess,
password = password
)
}
).show(screenWidth)
} else {
liveViewModel.enterRoom(roomId, onEnterRoomSuccess)
}
} else {
if (it.isPrivateRoom) {
LiveRoomPasswordDialog(
activity = this,
layoutInflater = layoutInflater,
can = it.price,
confirmButtonClick = { password ->
liveViewModel.enterRoom(
roomId = roomId,
onSuccess = onEnterRoomSuccess,
password = password
)
}
).show(screenWidth)
} else {
LivePaymentDialog(
activity = this,
layoutInflater = layoutInflater,
title = "${it.price.moneyFormat()}캔으로 입장",
desc = "'${it.title}' 라이브에 참여하기 위해 결제합니다.",
confirmButtonTitle = "결제 후 입장",
confirmButtonClick = {
liveViewModel.enterRoom(roomId, onEnterRoomSuccess)
},
cancelButtonTitle = "취소",
cancelButtonClick = {}
).show(screenWidth)
}
}
}
}
}
}

View File

@ -0,0 +1,139 @@
package kr.co.vividnext.sodalive.explorer.profile
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.RoundedCornersTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemUserProfileLiveBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
class UserProfileLiveAdapter(
private val onClickParticipant: (LiveRoomResponse) -> Unit,
private val onClickReservation: (LiveRoomResponse) -> Unit
) : RecyclerView.Adapter<UserProfileLiveAdapter.ViewHolder>() {
val items = mutableListOf<LiveRoomResponse>()
inner class ViewHolder(
private val context: Context,
private val binding: ItemUserProfileLiveBinding
) : RecyclerView.ViewHolder(binding.root) {
@SuppressLint("SetTextI18n")
fun bind(item: LiveRoomResponse) {
binding.ivCover.load(item.coverImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
transformations(RoundedCornersTransformation(4.7f.dpToPx()))
}
binding.iv19.visibility = if (item.isAdult) {
View.VISIBLE
} else {
View.GONE
}
binding.tvDate.text = item.beginDateTime
binding.tvNickname.text = item.managerNickname
binding.tvTitle.text = item.title
if (item.isActive && !item.channelName.isNullOrBlank()) {
binding.bgCover.visibility = View.GONE
binding.tvStatus.text = "LIVE"
binding.tvStatus.setTextColor(ContextCompat.getColor(context, R.color.color_ff5c49))
binding.tvStatus.setBackgroundResource(
R.drawable.bg_round_corner_3_3_transparent_ff5c49
)
binding.tvParticipate.text = if (!item.isPaid && item.price > 0) {
"${item.price}캔으로 지금 참여하기"
} else {
"지금 참여하기"
}
binding.tvParticipate.setOnClickListener { onClickParticipant(item) }
binding.tvParticipate.setTextColor(
ContextCompat.getColor(
context,
R.color.white
)
)
binding.tvParticipate.setBackgroundResource(
R.drawable.bg_round_corner_5_3_dd4500
)
} else if (item.isActive && item.channelName.isNullOrBlank()) {
binding.bgCover.visibility = View.GONE
binding.tvStatus.text = "예정"
binding.tvStatus.setTextColor(ContextCompat.getColor(context, R.color.color_fdca2f))
binding.tvStatus.setBackgroundResource(
R.drawable.bg_round_corner_3_3_transparent_fdca2f
)
if (item.isReservation) {
binding.tvParticipate.text = "예약완료"
binding.tvParticipate.setTextColor(
ContextCompat.getColor(context, R.color.color_777777)
)
binding.tvParticipate.setBackgroundResource(
R.drawable.bg_round_corner_5_3_525252
)
} else {
binding.tvParticipate.text = if (item.price > 0) {
"${item.price}캔으로 예약하기"
} else {
"예약하기"
}
binding.tvParticipate.setOnClickListener { onClickReservation(item) }
binding.tvParticipate.setTextColor(
ContextCompat.getColor(
context,
R.color.black
)
)
binding.tvParticipate.setBackgroundResource(
R.drawable.bg_round_corner_5_3_fdca2f
)
}
} else {
binding.bgCover.visibility = View.VISIBLE
binding.tvStatus.text = "종료"
binding.tvStatus.setTextColor(ContextCompat.getColor(context, R.color.color_777777))
binding.tvStatus.setBackgroundResource(
R.drawable.bg_round_corner_3_3_transparent_777777
)
binding.tvParticipate.text = "다시듣기를 지원하지 않습니다"
binding.tvParticipate.setTextColor(
ContextCompat.getColor(context, R.color.color_777777)
)
binding.tvParticipate.setBackgroundResource(
R.drawable.bg_round_corner_5_3_525252
)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
parent.context,
ItemUserProfileLiveBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
}
override fun getItemCount() = items.count()
}

View File

@ -0,0 +1,46 @@
package kr.co.vividnext.sodalive.explorer.profile
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.ItemUserProfileSimilarCreatorBinding
class UserProfileSimilarCreatorAdapter(
private val onClickItem: (SimilarCreatorResponse) -> Unit
) : RecyclerView.Adapter<UserProfileSimilarCreatorAdapter.ViewHolder>() {
val items = mutableListOf<SimilarCreatorResponse>()
inner class ViewHolder(
private val binding: ItemUserProfileSimilarCreatorBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: SimilarCreatorResponse) {
binding.ivProfile.load(item.profileImage) {
crossfade(true)
placeholder(R.drawable.ic_logo)
transformations(CircleCropTransformation())
}
binding.tvNickname.text = item.nickname
binding.tvTags.text = item.tags.joinToString(" ") { "#$it" }
binding.root.setOnClickListener { onClickItem(item) }
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
ItemUserProfileSimilarCreatorBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
}
override fun getItemCount() = items.count()
}

View File

@ -0,0 +1,375 @@
package kr.co.vividnext.sodalive.explorer.profile
import android.net.Uri
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.ktx.Firebase
import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.explorer.ExplorerRepository
import kr.co.vividnext.sodalive.report.ReportRepository
import kr.co.vividnext.sodalive.report.ReportRequest
import kr.co.vividnext.sodalive.report.ReportType
import kr.co.vividnext.sodalive.user.UserRepository
class UserProfileViewModel(
private val repository: ExplorerRepository,
private val reportRepository: ReportRepository,
private val userRepository: UserRepository
) : 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 _creatorProfileLiveData = MutableLiveData<GetCreatorProfileResponse>()
val creatorProfileLiveData: LiveData<GetCreatorProfileResponse>
get() = _creatorProfileLiveData
private val _isExpandNotice = MutableLiveData(false)
val isExpandNotice: LiveData<Boolean>
get() = _isExpandNotice
private var creatorNickname = ""
fun cheersReport(cheersId: Long, reason: String) {
_isLoading.value = true
val request = ReportRequest(ReportType.CHEERS, reason, cheersId = cheersId)
compositeDisposable.add(
reportRepository.report(
request = request,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
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 report(type: ReportType, userId: Long, reason: String = "프로필 신고") {
_isLoading.value = true
val request = ReportRequest(type, reason, reportedMemberId = userId)
compositeDisposable.add(
reportRepository.report(
request = request,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
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 getCreatorProfile(userId: Long, onFailure: (() -> Unit)? = null) {
_isLoading.value = true
compositeDisposable.add(
repository.getCreatorProfile(
id = userId,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
val data = it.data
_creatorProfileLiveData.postValue(data!!)
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
if (onFailure != null) {
onFailure()
}
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
if (onFailure != null) {
onFailure()
}
}
)
)
}
fun follow(creatorId: Long) {
_isLoading.value = true
compositeDisposable.add(
userRepository.creatorFollow(
creatorId,
"Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
getCreatorProfile(creatorId)
} 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 unFollow(creatorId: Long) {
_isLoading.value = true
compositeDisposable.add(
userRepository.creatorUnFollow(
creatorId,
"Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
getCreatorProfile(creatorId)
} 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 toggleExpandNotice() {
_isExpandNotice.value = !isExpandNotice.value!!
}
fun writeCheers(parentCheersId: Long? = null, creatorId: Long, cheersContent: String) {
if (cheersContent.isBlank()) {
_toastLiveData.postValue("내용을 입력하세요")
return
}
_isLoading.value = true
compositeDisposable.add(
repository.writeCheers(
parentCheersId = parentCheersId,
creatorId = creatorId,
content = cheersContent,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success) {
getCreatorProfile(creatorId)
} else {
val message = it.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
_toastLiveData.postValue(message)
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
fun modifyCheers(cheersId: Long, creatorId: Long, cheersContent: String) {
if (cheersContent.isBlank()) {
_toastLiveData.postValue("내용을 입력하세요")
return
}
_isLoading.value = true
compositeDisposable.add(
repository.modifyCheers(
cheersId = cheersId,
content = cheersContent,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success) {
getCreatorProfile(creatorId)
} else {
val message = it.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
_toastLiveData.postValue(message)
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
fun shareChannel(userId: Long, onSuccess: (String) -> Unit) {
_isLoading.value = true
Firebase.dynamicLinks.shortLinkAsync(ShortDynamicLink.Suffix.SHORT) {
link = Uri.parse("https://yozm.day/?channel_id=$userId")
domainUriPrefix = "https://yozm.page.link"
androidParameters { }
iosParameters("kr.co.vividnext.yozm") {
appStoreId = "1630284226"
}
}.addOnSuccessListener {
val uri = it.shortLink
if (uri != null) {
onSuccess("요즘라이브 ${creatorNickname}님의 채널입니다.\n$uri")
}
}.addOnFailureListener {
_toastLiveData.postValue("공유링크를 생성하지 못했습니다.\n다시 시도해 주세요.")
}.addOnCompleteListener {
_isLoading.value = false
}
}
fun userBlock(userId: Long) {
_isLoading.value = true
compositeDisposable.add(
userRepository.memberBlock(
userId = userId,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success) {
getCreatorProfile(userId)
_toastLiveData.postValue("차단하였습니다.")
} else {
val message = it.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
_toastLiveData.postValue(message)
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
fun userUnBlock(userId: Long) {
_isLoading.value = true
compositeDisposable.add(
userRepository.memberUnBlock(
userId = userId,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success) {
getCreatorProfile(userId)
_toastLiveData.postValue("차단이 해제 되었습니다.")
} else {
val message = it.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
_toastLiveData.postValue(message)
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
}

View File

@ -0,0 +1,9 @@
package kr.co.vividnext.sodalive.explorer.profile.cheers
import com.google.gson.annotations.SerializedName
data class PostWriteCheersRequest(
@SerializedName("parentId") val parentId: Long? = null,
@SerializedName("creatorId") val creatorId: Long,
@SerializedName("content") val content: String
)

View File

@ -0,0 +1,8 @@
package kr.co.vividnext.sodalive.explorer.profile.cheers
import com.google.gson.annotations.SerializedName
data class PutModifyCheersRequest(
@SerializedName("cheersId") val cheersId: Long,
@SerializedName("content") val content: String
)

View File

@ -0,0 +1,126 @@
package kr.co.vividnext.sodalive.explorer.profile.cheers
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.PopupMenu
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.RoundedCornersTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ItemUserProfileCheersBinding
import kr.co.vividnext.sodalive.explorer.profile.GetCheersResponseItem
import kr.co.vividnext.sodalive.extensions.dpToPx
class UserProfileCheersAdapter(
private val userId: Long,
private val enterReply: (Long, String) -> Unit,
private val modifyReply: (Long, String) -> Unit,
private val onClickReport: (Long) -> Unit
) : RecyclerView.Adapter<UserProfileCheersAdapter.ViewHolder>() {
val items = mutableListOf<GetCheersResponseItem>()
inner class ViewHolder(
private val context: Context,
private val binding: ItemUserProfileCheersBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(cheers: GetCheersResponseItem) {
binding.tvWriteReply.visibility = View.GONE
binding.llCheerReply.visibility = View.GONE
binding.rlCheerReply.visibility = View.GONE
binding.ivProfile.load(cheers.profileUrl) {
crossfade(true)
placeholder(R.drawable.ic_logo)
transformations(RoundedCornersTransformation(16.7f.dpToPx()))
}
binding.tvContent.text = cheers.content
binding.tvNickname.text = cheers.nickname
binding.tvDate.text = cheers.date
binding.ivMenu.setOnClickListener {
showOptionMenu(
context,
binding.ivMenu,
cheersId = cheers.cheersId
)
}
if (cheers.replyList.isNotEmpty()) {
binding.tvWriteReply.visibility = View.GONE
binding.llCheerReply.visibility = View.VISIBLE
val reply = cheers.replyList[0]
binding.tvReply.text = reply.content
binding.tvReplyDate.text = reply.date
if (userId == SharedPreferenceManager.userId) {
binding.tvModifyReply.visibility = View.VISIBLE
binding.tvModifyReply.setOnClickListener {
binding.etCheerReply.setText(binding.tvReply.text)
binding.rlCheerReply.visibility = View.VISIBLE
binding.tvSend.setOnClickListener {
val content = binding.etCheerReply.text.toString()
modifyReply(reply.cheersId, content)
}
}
} else {
binding.tvModifyReply.visibility = View.GONE
}
} else {
if (userId == SharedPreferenceManager.userId) {
binding.tvWriteReply.visibility = View.VISIBLE
binding.tvWriteReply.setOnClickListener {
binding.tvWriteReply.visibility = View.GONE
binding.rlCheerReply.visibility = View.VISIBLE
binding.tvSend.setOnClickListener {
val content = binding.etCheerReply.text.toString()
enterReply(cheers.cheersId, content)
}
}
} else {
binding.tvWriteReply.visibility = View.GONE
}
}
binding.tvReply.requestLayout()
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
parent.context,
ItemUserProfileCheersBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
}
override fun getItemCount() = items.count()
private fun showOptionMenu(context: Context, v: View, cheersId: Long) {
val popup = PopupMenu(context, v)
val inflater = popup.menuInflater
inflater.inflate(R.menu.review_option_menu, popup.menu)
popup.setOnMenuItemClickListener {
when (it.itemId) {
R.id.menu_review_report -> {
onClickReport(cheersId)
}
}
true
}
popup.show()
}
}

View File

@ -0,0 +1,76 @@
package kr.co.vividnext.sodalive.explorer.profile.donation
import android.view.LayoutInflater
import android.view.View
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.ItemUserProfileDonationBinding
import kr.co.vividnext.sodalive.explorer.profile.UserDonationRankingResponse
class UserProfileDonationAdapter : RecyclerView.Adapter<UserProfileDonationAdapter.ViewHolder>() {
val items = mutableListOf<UserDonationRankingResponse>()
inner class ViewHolder(
private val binding: ItemUserProfileDonationBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: UserDonationRankingResponse, position: Int) {
binding.tvNickname.text = item.nickname
binding.ivProfile.load(item.profileImage) {
crossfade(true)
placeholder(R.drawable.ic_logo)
transformations(CircleCropTransformation())
}
when (position) {
0 -> {
binding.ivBg.setImageResource(R.drawable.bg_circle_ffdc00_ffb600)
binding.ivBg.visibility = View.VISIBLE
binding.ivCrown.setImageResource(R.drawable.ic_crown_1)
binding.ivCrown.visibility = View.VISIBLE
}
1 -> {
binding.ivBg.setImageResource(R.drawable.bg_circle_ffffff_9f9f9f)
binding.ivBg.visibility = View.VISIBLE
binding.ivCrown.setImageResource(R.drawable.ic_crown_2)
binding.ivCrown.visibility = View.VISIBLE
}
2 -> {
binding.ivBg.setImageResource(R.drawable.bg_circle_e6a77a_c67e4a)
binding.ivBg.visibility = View.VISIBLE
binding.ivCrown.setImageResource(R.drawable.ic_crown_3)
binding.ivCrown.visibility = View.VISIBLE
}
else -> {
binding.ivBg.setImageResource(0)
binding.ivBg.visibility = View.GONE
binding.ivCrown.visibility = View.GONE
}
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
ItemUserProfileDonationBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position], position)
}
override fun getItemCount() = items.count()
}

View File

@ -0,0 +1,10 @@
package kr.co.vividnext.sodalive.explorer.profile.donation
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.databinding.ActivityUserProfileLiveAllBinding
class UserProfileDonationAllViewActivity : BaseActivity<ActivityUserProfileLiveAllBinding>(
ActivityUserProfileLiveAllBinding::inflate
) {
override fun setupView() {}
}

View File

@ -0,0 +1,10 @@
package kr.co.vividnext.sodalive.explorer.profile.fantalk
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.databinding.ActivityUserProfileFantalkAllBinding
class UserProfileFantalkAllViewActivity : BaseActivity<ActivityUserProfileFantalkAllBinding>(
ActivityUserProfileFantalkAllBinding::inflate
) {
override fun setupView() {}
}

View File

@ -0,0 +1,15 @@
package kr.co.vividnext.sodalive.explorer.profile.follow
import com.google.gson.annotations.SerializedName
data class GetFollowerListResponse(
@SerializedName("totalCount") val totalCount: Int,
@SerializedName("items") val items: List<GetFollowerListResponseItem>
)
data class GetFollowerListResponseItem(
@SerializedName("userId") val userId: Long,
@SerializedName("profileImage") val profileImage: String,
@SerializedName("nickname") val nickname: String,
@SerializedName("isFollow") val isFollow: Boolean?
)

View File

@ -0,0 +1,116 @@
package kr.co.vividnext.sodalive.explorer.profile.follow
import android.graphics.Rect
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.ActivityUserFollowerListBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.moneyFormat
import org.koin.android.ext.android.inject
class UserFollowerListActivity : BaseActivity<ActivityUserFollowerListBinding>(
ActivityUserFollowerListBinding::inflate
) {
private val viewModel: UserFollowerListViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var adapter: UserFollowerListAdapter
private var userId: Long = 0
override fun onCreate(savedInstanceState: Bundle?) {
userId = intent.getLongExtra(Constants.EXTRA_USER_ID, 0)
if (userId <= 0) {
Toast.makeText(
applicationContext,
"잘못된 요청입니다.\n다시 시도해 주세요.",
Toast.LENGTH_LONG
).show()
finish()
}
super.onCreate(savedInstanceState)
bindData()
viewModel.getFollowerList()
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.text = "팔로워 리스트"
binding.toolbar.tvBack.setOnClickListener { finish() }
adapter = UserFollowerListAdapter(
onClickRegisterNotification = { viewModel.registerNotification(it) },
onClickUnRegisterNotification = { viewModel.unRegisterNotification(it) }
)
binding.rvFollowerList.layoutManager = LinearLayoutManager(
applicationContext,
LinearLayoutManager.VERTICAL,
false
)
binding.rvFollowerList.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val layoutManager = recyclerView.layoutManager as? LinearLayoutManager
if (
layoutManager != null &&
layoutManager.findLastCompletelyVisibleItemPosition() == adapter.itemCount - 1
) {
viewModel.getFollowerList()
}
}
})
binding.rvFollowerList.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
outRect.left = 20f.dpToPx().toInt()
outRect.right = 20f.dpToPx().toInt()
}
})
binding.rvFollowerList.adapter = adapter
}
private fun bindData() {
viewModel.userId = userId
viewModel.followerListItemsLiveData.observe(this) {
adapter.addAll(it)
}
viewModel.totalCountLiveData.observe(this) {
binding.tvCount.text = it.moneyFormat()
}
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()
}
}
}
}

View File

@ -0,0 +1,75 @@
package kr.co.vividnext.sodalive.explorer.profile.follow
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.CircleCropTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemFollowerListBinding
class UserFollowerListAdapter(
private val onClickRegisterNotification: (Long) -> Unit,
private val onClickUnRegisterNotification: (Long) -> Unit,
) : RecyclerView.Adapter<UserFollowerListAdapter.ViewHolder>() {
private val items = mutableListOf<GetFollowerListResponseItem>()
inner class ViewHolder(
private val binding: ItemFollowerListBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetFollowerListResponseItem) {
binding.tvNickname.text = item.nickname
binding.ivProfile.load(item.profileImage) {
transformations(CircleCropTransformation())
placeholder(R.drawable.ic_logo)
crossfade(true)
}
if (item.isFollow != null) {
binding.ivNotification.visibility = View.VISIBLE
if (item.isFollow) {
binding.ivNotification.setImageResource(R.drawable.btn_notification_selected)
binding.ivNotification.setOnClickListener {
onClickUnRegisterNotification(item.userId)
clear()
}
} else {
binding.ivNotification.setImageResource(R.drawable.btn_notification)
binding.ivNotification.setOnClickListener {
onClickRegisterNotification(item.userId)
clear()
}
}
} else {
binding.ivNotification.visibility = View.GONE
}
}
}
@SuppressLint("NotifyDataSetChanged")
fun addAll(items: List<GetFollowerListResponseItem>) {
this.items.addAll(items)
notifyDataSetChanged()
}
fun clear() {
this.items.clear()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
ItemFollowerListBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
}
override fun getItemCount() = items.size
}

View File

@ -0,0 +1,156 @@
package kr.co.vividnext.sodalive.explorer.profile.follow
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.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.explorer.ExplorerRepository
import kr.co.vividnext.sodalive.user.UserRepository
class UserFollowerListViewModel(
private val repository: ExplorerRepository,
private val userRepository: UserRepository
) : 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 _totalCountLiveData = MutableLiveData(0)
val totalCountLiveData: LiveData<Int>
get() = _totalCountLiveData
private var _followerListItemsLiveData = MutableLiveData<List<GetFollowerListResponseItem>>()
val followerListItemsLiveData: LiveData<List<GetFollowerListResponseItem>>
get() = _followerListItemsLiveData
var userId: Long = 0
var page = 1
private var isLast = false
private val pageSize = 10
fun getFollowerList() {
if (!isLast && !_isLoading.value!!) {
_isLoading.value = true
compositeDisposable.add(
repository.getFollowerList(
userId,
page,
pageSize,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success && it.data != null) {
val data = it.data
_totalCountLiveData.value = data.totalCount
if (data.items.isEmpty()) {
isLast = true
} else {
page += 1
_followerListItemsLiveData.value = data.items
}
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
)
)
}
}
fun registerNotification(creatorId: Long) {
_isLoading.value = true
compositeDisposable.add(
userRepository.creatorFollow(
creatorId,
"Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success && it.data != null) {
page = 1
isLast = false
getFollowerList()
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
fun unRegisterNotification(creatorId: Long) {
_isLoading.value = true
compositeDisposable.add(
userRepository.creatorUnFollow(
creatorId,
"Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success && it.data != null) {
page = 1
isLast = false
getFollowerList()
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
}

Some files were not shown because too many files have changed in this diff Show More