Compare commits

...

60 Commits

Author SHA1 Message Date
klaus b876695adc 회사정보 변경 2024-05-21 01:25:17 +09:00
klaus b220b8bce4 콘텐츠 상세, 콘텐츠 구매
- pg 테스트 계정의 경우 캔이 아닌 원으로 표시되도록 하고 콘텐츠 구매시 바로 결제 후 구매 되도록 수정
2024-05-20 15:37:46 +09:00
klaus ba4d707d45 9970ff -> 3bb9f1 2024-05-17 15:35:26 +09:00
klaus 2920d2a7ae 9970ff -> 3bb9f1 2024-05-17 15:28:59 +09:00
klaus b8abef6aeb 9970ff -> 3bb9f1 2024-05-17 15:11:41 +09:00
klaus 75fc34cbe3 9970ff -> 3bb9f1 2024-05-17 15:09:47 +09:00
klaus 206fc398c6 라이브, 회원 태그
- 9970ff -> 3bb9f1 색상 변경
2024-05-16 14:23:07 +09:00
klaus b95b77bcb9 룰렛 설정
- 옵션 확률 총합 추가
2024-05-16 12:41:20 +09:00
klaus 62c269cac2 룰렛 설정
- 옵션 추가시 확률이 빈칸으로 표시되어 NumberFormat Exception이 나오던 버그 예외처리
2024-05-16 12:11:05 +09:00
klaus 4b3474ff42 라이브 방 생성
- 크리에이터 입장 가능 설정 추가
2024-05-14 17:06:12 +09:00
klaus d6e9b929e9 라이브 방
- 음소거 버튼 위치 아래로 이동
2024-05-11 04:00:26 +09:00
klaus 13efce42a0 룰렛 설정
- 룰렛 1, 2, 3 버튼 bg, text 색상 변경
2024-05-11 03:55:58 +09:00
klaus 85ccc18485 룰렛 변경
- 확률 수동 설정
- 여러개의 룰렛이 켜져있을 때 선택하여 돌리기
- 후원 히스토리에 룰렛 히스토리 추가
2024-05-10 17:55:20 +09:00
klaus 9df08cdf24 콘텐츠 메인
- 새로운 콘텐츠 아래 새로고침 버튼 제거
2024-05-08 12:17:28 +09:00
klaus 7892eed443 라이브
- 라이브가 없을 때 문구 수정
- 라이브가 없을 떄 문구 자간 및 폰트 사이즈 수정
2024-05-08 12:15:13 +09:00
klaus eca68c06c3 라이브 메인 - 라이브 없을 때 문구
- 🙀마이페이지에서 본인인증을 하거나 라이브를 예약하고 참여해보세요.
2024-05-07 19:27:34 +09:00
klaus 3c82ff1c4e 라이브 메인
- 당겨서 새로고침 제거
- 새로고침 버튼 추가
2024-05-07 19:02:58 +09:00
klaus 8c6aff1623 콘텐츠 메인 - 추천 시리즈, 새로운 콘텐츠
- 새로고침 버튼 추가
2024-05-07 18:58:15 +09:00
klaus 254a1e3381 콘텐츠 메인
- 추천 시리즈 UI 추가
2024-05-07 16:42:14 +09:00
klaus dff4c833f1 . 2024-05-04 02:47:03 +09:00
klaus 5fa3a591d4 인 앱 결제
- 서버 호출 후 충전이 완료되면 충전완료 페이지로 이동하도록 수정
2024-05-03 19:47:19 +09:00
klaus 845578a1dd 시리즈 아이템
- 커버이미지 DIM 제거
2024-05-03 13:41:30 +09:00
klaus 84ac72b391 탐색
- 크리에이터가 없으면 섹션제거
2024-05-03 13:36:41 +09:00
klaus db364d9bf7 라이브
- 시그니처 ON/OFF 버튼 추가
- 공유하기 버튼 제거
2024-05-02 15:17:00 +09:00
klaus 3fe01f8def 2024년 5월 인트로 적용 2024-05-01 22:44:16 +09:00
klaus 02c815077e 시그니처 후원
- 시그니처 별로 설정된 시간 만큼 GIF가 재생되도록 기능 추가
2024-05-01 22:14:58 +09:00
klaus 108eb759ec 시리즈 전체보기
아이템 세로 간격 수정
2024-05-01 00:39:17 +09:00
klaus fef49a0d6a 시리즈 전체보기
아이템 사이즈 수정
2024-04-30 22:33:37 +09:00
klaus 87241fa8bd 시그니처 후원
- 위치 가운데로 수정
2024-04-30 19:55:47 +09:00
klaus 29d5192fff 시리즈 콘텐츠
- 대여중/소장중/가격 뱃지가 동시에 표시되는 버그 수정
2024-04-30 19:28:16 +09:00
klaus c86e55719e 인 앱 결제
- 사용하지 않는 함수 제거
2024-04-30 17:18:19 +09:00
klaus 839a8a780c 시리즈 상세 작품소개
- 0원 -> 무료로 변경
2024-04-30 14:57:32 +09:00
klaus 346334a0ba 시리즈 상세 작품소개
- 구분선 색 변경
2024-04-30 13:32:10 +09:00
klaus 7cd4d180c2 시리즈 상세 콘텐츠
- 제목이 가격영역을 침범하는 버그 수정
2024-04-29 11:47:24 +09:00
klaus 320ef4fbc7 시리즈 상세
- 19금과 전체연령가가 반대로 표시되던 버그 수정
2024-04-29 11:41:13 +09:00
klaus b4623141f3 크리에이터 채널
- 시리즈 데이터가 파싱되지 않던 버그 수정
2024-04-27 04:23:38 +09:00
klaus 30d3ed14d7 시리즈 콘텐츠 리스트 아이템
- 출시 날짜 제거
- 재생 시간 제목 윗쪽으로 이동
2024-04-27 03:14:09 +09:00
klaus ae617d5154 크리에이터 채널 시리즈
- 이미지 사이즈
가로 102 -> 116.7, 세로 144 -> 165 변경
2024-04-27 03:10:51 +09:00
klaus 9dd3c568d8 시리즈 상세
- 스크롤시 타이틀 윗쪽에 배경이 살짝 보이던 버그 수정
2024-04-27 02:54:39 +09:00
klaus f31fc7691e 시리즈 상세
- 내용 스크롤 시 뒤로가기 버튼도 같이 스크롤 되도록 수정
2024-04-27 02:01:53 +09:00
klaus 27df922383 시리즈 상세 작품소개
- 키워드 세로 간격 수정
- 상세정보 글자색 흰색으로 변경
2024-04-27 01:57:54 +09:00
klaus d5956c024d 시리즈 상세
- 팔로잉 액션 추가
2024-04-27 01:25:02 +09:00
klaus 29aca74651 시리즈 상세
- 전체회차 보기 사이즈 16으로 변경
- 태그 글자 크기 12로 변경
2024-04-27 01:08:22 +09:00
klaus f41790b302 시리즈 콘텐츠 전체보기
- 제목 - 전체회차 듣기 로 변경
2024-04-27 00:47:44 +09:00
klaus 0b999a874c 시리즈 콘텐츠 전체보기 페이지 추가 2024-04-27 00:34:42 +09:00
klaus 2778638dc9 시리즈 상세보기
- 콘텐츠 링크 추가
2024-04-26 23:27:26 +09:00
klaus cc5fe445fc 시리즈 상세보기 페이지 추가 2024-04-26 23:17:44 +09:00
klaus c310f9c57e 시리즈 전체보기 페이지 추가 2024-04-25 22:05:58 +09:00
klaus cd607425a0 크리에이터 채널
- 시리즈 section 추가
2024-04-25 17:14:19 +09:00
klaus a5df8a1110 크리에이터 채널
- 활동요약표 선 색깔 button색으로 수정
2024-04-22 15:05:16 +09:00
klaus 2bd30aa346 versionCode 57, versionName 1.9.11
인 앱 결제 로직 수정
- 결제 완료 후 서버에서 데이터 처리 후 로컬에서 다시 소비처리를 하도록 수정
2024-04-22 14:40:16 +09:00
klaus a6ce994fd0 versionCode 49, versionName 1.9.2 2024-04-12 19:57:44 +09:00
klaus 7bffd1c3c7 후원하기 팝업
- 캔 충전하기 페이지로 이동하는 버튼 직관적으로 보이도록 화살표에서 충전 글자로 변경
2024-04-12 14:30:30 +09:00
klaus dfd92d6db6 음소거 버튼 위치 - top
크리에이터 팔로우 버튼 margin_top = 5.3
시그니처 후원 움짤 위치 - 우측 중간으로 이동
2024-04-12 14:09:47 +09:00
klaus 5529872bd5 본인인증을 하지 않아도 PG결제가 보이도록 수정 2024-04-05 11:46:18 +09:00
klaus 364a530956 크리에이터 커뮤니티 대댓글
- 부모 댓글과 동일한 글자 색상으로 변경
2024-04-02 15:36:49 +09:00
klaus 0556d5a067 오픈 예정 콘텐츠 상세
- 댓글 창, 좋아요, 공유, 후원 버튼 숨김
2024-04-02 14:47:56 +09:00
klaus be46893555 캔 충전 페이지
- PG, IAP 순서로 탭 변경
2024-04-02 00:33:12 +09:00
klaus f7f789892d okhttp connect, read, write timeout 시간 60초 설정 2024-04-01 16:38:58 +09:00
klaus de0d327168 인트로
- 4월 인트로로 변경
2024-04-01 15:48:06 +09:00
140 changed files with 4756 additions and 998 deletions

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates>
</component>
</project>

View File

@ -40,8 +40,8 @@ android {
applicationId "kr.co.vividnext.sodalive" applicationId "kr.co.vividnext.sodalive"
minSdk 23 minSdk 23
targetSdk 33 targetSdk 33
versionCode 46 versionCode 74
versionName "1.8.21" versionName "1.11.3"
} }
buildTypes { buildTypes {

View File

@ -89,6 +89,7 @@
android:name=".mypage.can.charge.CanChargeActivity" android:name=".mypage.can.charge.CanChargeActivity"
android:configChanges="orientation|screenSize|keyboardHidden" /> android:configChanges="orientation|screenSize|keyboardHidden" />
<activity android:name=".mypage.can.payment.CanPaymentActivity" /> <activity android:name=".mypage.can.payment.CanPaymentActivity" />
<activity android:name=".mypage.can.payment.CanPaymentTempActivity" />
<activity android:name=".mypage.can.coupon.CanCouponActivity" /> <activity android:name=".mypage.can.coupon.CanCouponActivity" />
<activity android:name=".live.room.create.LiveRoomCreateActivity" /> <activity android:name=".live.room.create.LiveRoomCreateActivity" />
<activity android:name=".live.room.update.LiveRoomEditActivity" /> <activity android:name=".live.room.update.LiveRoomEditActivity" />
@ -133,6 +134,9 @@
<activity android:name=".audio_content.all.AudioContentRankingAllActivity" /> <activity android:name=".audio_content.all.AudioContentRankingAllActivity" />
<activity android:name=".audio_content.all.by_theme.AudioContentAllByThemeActivity" /> <activity android:name=".audio_content.all.by_theme.AudioContentAllByThemeActivity" />
<activity android:name=".live.roulette.config.RouletteConfigActivity" /> <activity android:name=".live.roulette.config.RouletteConfigActivity" />
<activity android:name=".audio_content.series.SeriesListAllActivity" />
<activity android:name=".audio_content.series.detail.SeriesDetailActivity" />
<activity android:name=".audio_content.series.content.SeriesContentAllActivity" />
<activity <activity
android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity" android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity"

View File

@ -9,6 +9,7 @@ import android.content.Intent
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.media.AudioAttributes import android.media.AudioAttributes
import android.media.AudioManager
import android.media.MediaPlayer import android.media.MediaPlayer
import android.os.Build import android.os.Build
import android.os.Handler import android.os.Handler
@ -367,6 +368,7 @@ class AudioContentPlayService :
mediaPlayer.setOnCompletionListener(this) mediaPlayer.setOnCompletionListener(this)
mediaPlayer.setAudioAttributes( mediaPlayer.setAudioAttributes(
AudioAttributes.Builder() AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build() .build()
) )

View File

@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.audio_content.detail package kr.co.vividnext.sodalive.audio_content.detail
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@ -13,6 +14,8 @@ import android.view.View
import android.widget.RelativeLayout import android.widget.RelativeLayout
import android.widget.SeekBar import android.widget.SeekBar
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -37,13 +40,16 @@ import kr.co.vividnext.sodalive.common.Utils
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentDetailBinding import kr.co.vividnext.sodalive.databinding.ActivityAudioContentDetailBinding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
import kr.co.vividnext.sodalive.extensions.dpToPx import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.moneyFormat
import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationDialog import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationDialog
import kr.co.vividnext.sodalive.mypage.auth.Auth import kr.co.vividnext.sodalive.mypage.auth.Auth
import kr.co.vividnext.sodalive.mypage.auth.AuthVerifyRequest import kr.co.vividnext.sodalive.mypage.auth.AuthVerifyRequest
import kr.co.vividnext.sodalive.mypage.auth.BootpayResponse import kr.co.vividnext.sodalive.mypage.auth.BootpayResponse
import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeActivity import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeActivity
import kr.co.vividnext.sodalive.mypage.can.payment.CanPaymentTempActivity
import kr.co.vividnext.sodalive.report.ReportType import kr.co.vividnext.sodalive.report.ReportType
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import kotlin.math.ceil
class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBinding>( class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBinding>(
ActivityAudioContentDetailBinding::inflate ActivityAudioContentDetailBinding::inflate
@ -63,6 +69,10 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
private var refresh = false private var refresh = false
private var title = "" private var title = ""
private lateinit var activityResultLauncher: ActivityResultLauncher<Intent>
private lateinit var audioContent: GetAudioContentDetailResponse
private lateinit var orderType: OrderType
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun onNewIntent(intent: Intent) { override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent) super.onNewIntent(intent)
@ -87,6 +97,14 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
finish() finish()
} }
activityResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode == Activity.RESULT_OK) {
contentOrder(audioContent, orderType)
}
}
bindData() bindData()
viewModel.getAudioContentDetail(audioContentId = audioContentId) { finish() } viewModel.getAudioContentDetail(audioContentId = audioContentId) { finish() }
} }
@ -433,7 +451,11 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
} }
private fun setupCommentArea(response: GetAudioContentDetailResponse) { private fun setupCommentArea(response: GetAudioContentDetailResponse) {
if (response.isCommentAvailable) { if (
response.isCommentAvailable &&
response.contentUrl.isNotBlank() &&
response.releaseDate == null
) {
binding.llDonation.visibility = View.VISIBLE binding.llDonation.visibility = View.VISIBLE
binding.llComment.visibility = View.VISIBLE binding.llComment.visibility = View.VISIBLE
binding.tvCommentCount.text = "${response.commentCount}" binding.tvCommentCount.text = "${response.commentCount}"
@ -512,12 +534,29 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
binding.tvReleaseDate.visibility = View.GONE binding.tvReleaseDate.visibility = View.GONE
binding.llPurchase.visibility = View.VISIBLE binding.llPurchase.visibility = View.VISIBLE
binding.llPurchasePrice.visibility = View.VISIBLE binding.llPurchasePrice.visibility = View.VISIBLE
binding.tvPrice.text = response.price.toString()
binding.llPurchase.background = ContextCompat.getDrawable( binding.llPurchase.background = ContextCompat.getDrawable(
applicationContext, applicationContext,
R.drawable.bg_round_corner_5_3_3bb9f1 R.drawable.bg_round_corner_5_3_3bb9f1
) )
binding.ivCan.visibility = if (SharedPreferenceManager.userId == 17958L) {
View.GONE
} else {
View.VISIBLE
}
binding.tvPrice.text = if (SharedPreferenceManager.userId == 17958L) {
(response.price * 110).moneyFormat()
} else {
response.price.moneyFormat()
}
binding.tvUnit.text = if (SharedPreferenceManager.userId == 17958L) {
"원으로"
} else {
"캔으로"
}
binding.tvStrPurchaseOrRental.text = if (response.isOnlyRental) { binding.tvStrPurchaseOrRental.text = if (response.isOnlyRental) {
" 대여하기" " 대여하기"
} else { } else {
@ -657,45 +696,54 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
binding.tvTag.visibility = View.GONE binding.tvTag.visibility = View.GONE
} }
binding.ivLike.setImageResource( if (response.contentUrl.isNotBlank() && response.releaseDate == null) {
if (response.isLike) { binding.svActionButtons.visibility = View.VISIBLE
R.drawable.ic_audio_content_heart_pressed binding.llLike.visibility = View.VISIBLE
} else { binding.ivLike.setImageResource(
R.drawable.ic_audio_content_heart_normal if (response.isLike) {
} R.drawable.ic_audio_content_heart_pressed
)
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 { } else {
binding.tvLike.text = if (likeCount - 1 < 0) { R.drawable.ic_audio_content_heart_normal
"0" }
)
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 { } else {
"${likeCount - 1}" binding.tvLike.text = if (likeCount - 1 < 0) {
"0"
} else {
"${likeCount - 1}"
}
binding.ivLike.setImageResource(R.drawable.ic_audio_content_heart_normal)
} }
binding.ivLike.setImageResource(R.drawable.ic_audio_content_heart_normal)
} }
} }
}
binding.tvShare.setOnClickListener { binding.tvShare.visibility = View.VISIBLE
viewModel.shareAudioContent( binding.tvShare.setOnClickListener {
audioContentId = audioContentId, viewModel.shareAudioContent(
contentImage = response.coverImageUrl, audioContentId = audioContentId,
contentTitle = "${response.title} - ${response.creator.nickname}" contentImage = response.coverImageUrl,
) { contentTitle = "${response.title} - ${response.creator.nickname}"
val intent = Intent(Intent.ACTION_SEND) ) {
intent.type = "text/plain" val intent = Intent(Intent.ACTION_SEND)
intent.putExtra(Intent.EXTRA_TEXT, it) intent.type = "text/plain"
intent.putExtra(Intent.EXTRA_TEXT, it)
val shareIntent = Intent.createChooser(intent, "오디오콘텐츠 공유") val shareIntent = Intent.createChooser(intent, "오디오콘텐츠 공유")
startActivity(shareIntent) startActivity(shareIntent)
}
} }
} else {
binding.svActionButtons.visibility = View.GONE
binding.llLike.visibility = View.GONE
binding.tvShare.visibility = View.GONE
} }
if (response.totalContentCount != null && response.remainingContentCount != null) { if (response.totalContentCount != null && response.remainingContentCount != null) {
@ -834,18 +882,43 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
binding.rlPreviewAlert.visibility = View.GONE binding.rlPreviewAlert.visibility = View.GONE
viewModel.order( if (SharedPreferenceManager.userId == 17958L) {
contentId = audioContent.contentId, this@AudioContentDetailActivity.audioContent = audioContent
orderType = orderType this@AudioContentDetailActivity.orderType = orderType
) { activityResultLauncher.launch(
val intent = Intent(applicationContext, CanChargeActivity::class.java) Intent(applicationContext, CanPaymentTempActivity::class.java).apply {
intent.putExtra(Constants.EXTRA_GO_TO_PREV_PAGE, true) putExtra("title", audioContent.title)
startActivity(intent) putExtra(
"can",
if (orderType == OrderType.RENTAL) {
ceil(audioContent.price * 0.6).toInt()
} else {
audioContent.price
}
)
}
)
} else {
contentOrder(audioContent, orderType)
} }
}, },
).show(screenWidth) ).show(screenWidth)
} }
private fun contentOrder(
audioContent: GetAudioContentDetailResponse,
orderType: OrderType
) {
viewModel.order(
contentId = audioContent.contentId,
orderType = orderType
) {
val intent = Intent(applicationContext, CanChargeActivity::class.java)
intent.putExtra(Constants.EXTRA_GO_TO_PREV_PAGE, true)
startActivity(intent)
}
}
inner class AudioContentReceiver : BroadcastReceiver() { inner class AudioContentReceiver : BroadcastReceiver() {
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun onReceive(context: Context?, intent: Intent?) { override fun onReceive(context: Context?, intent: Intent?) {

View File

@ -1,13 +1,11 @@
package kr.co.vividnext.sodalive.audio_content.main package kr.co.vividnext.sodalive.audio_content.main
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Service
import android.content.Intent import android.content.Intent
import android.graphics.Rect import android.graphics.Rect
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.Toast import android.widget.Toast
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@ -29,18 +27,19 @@ import kr.co.vividnext.sodalive.audio_content.main.curation.AudioContentMainCura
import kr.co.vividnext.sodalive.audio_content.main.curation.AudioContentMainCurationViewModel import kr.co.vividnext.sodalive.audio_content.main.curation.AudioContentMainCurationViewModel
import kr.co.vividnext.sodalive.audio_content.main.new_content.AudioContentMainNewContentThemeAdapter import kr.co.vividnext.sodalive.audio_content.main.new_content.AudioContentMainNewContentThemeAdapter
import kr.co.vividnext.sodalive.audio_content.main.new_content.AudioContentMainNewContentViewModel import kr.co.vividnext.sodalive.audio_content.main.new_content.AudioContentMainNewContentViewModel
import kr.co.vividnext.sodalive.audio_content.main.new_content_upload_creator.AudioContentMainNewContentCreatorAdapter
import kr.co.vividnext.sodalive.audio_content.main.new_content_upload_creator.AudioContentMainNewContentCreatorViewModel
import kr.co.vividnext.sodalive.audio_content.main.order.AudioContentMainOrderListViewModel import kr.co.vividnext.sodalive.audio_content.main.order.AudioContentMainOrderListViewModel
import kr.co.vividnext.sodalive.audio_content.main.ranking.AudioContentMainRankingAdapter import kr.co.vividnext.sodalive.audio_content.main.ranking.AudioContentMainRankingAdapter
import kr.co.vividnext.sodalive.audio_content.main.ranking.AudioContentMainRankingViewModel import kr.co.vividnext.sodalive.audio_content.main.ranking.AudioContentMainRankingViewModel
import kr.co.vividnext.sodalive.audio_content.main.recommend_series.AudioContentMainRecommendSeriesViewModel
import kr.co.vividnext.sodalive.audio_content.order.AudioContentOrderListActivity import kr.co.vividnext.sodalive.audio_content.order.AudioContentOrderListActivity
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadActivity import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadActivity
import kr.co.vividnext.sodalive.base.BaseFragment import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.common.Constants import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.FragmentAudioContentMainBinding import kr.co.vividnext.sodalive.databinding.FragmentAudioContentMainBinding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
import kr.co.vividnext.sodalive.explorer.profile.series.UserProfileSeriesListAdapter
import kr.co.vividnext.sodalive.extensions.dpToPx import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.settings.event.EventDetailActivity import kr.co.vividnext.sodalive.settings.event.EventDetailActivity
import kr.co.vividnext.sodalive.settings.notification.MemberRole import kr.co.vividnext.sodalive.settings.notification.MemberRole
@ -50,8 +49,8 @@ import kotlin.math.roundToInt
class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>( class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
FragmentAudioContentMainBinding::inflate FragmentAudioContentMainBinding::inflate
) { ) {
private val newContentCreatorViewModel: AudioContentMainNewContentCreatorViewModel by inject() private val recommendSeriesViewModel: AudioContentMainRecommendSeriesViewModel by inject()
private lateinit var newContentCreatorAdapter: AudioContentMainNewContentCreatorAdapter private lateinit var seriesAdapter: UserProfileSeriesListAdapter
private val bannerViewModel: AudioContentMainBannerViewModel by inject() private val bannerViewModel: AudioContentMainBannerViewModel by inject()
private lateinit var bannerAdapter: AudioContentMainBannerAdapter private lateinit var bannerAdapter: AudioContentMainBannerAdapter
@ -80,7 +79,7 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
newContentViewModel.getNewContentOfTheme("전체") newContentViewModel.getNewContentOfTheme("전체")
contentRankingViewModel.getContentRanking() contentRankingViewModel.getContentRanking()
contentRankingViewModel.getContentRankingSortType() contentRankingViewModel.getContentRankingSortType()
newContentCreatorViewModel.getNewContentUploadCreatorList() recommendSeriesViewModel.getRecommendSeriesList()
} }
private fun setupView() { private fun setupView() {
@ -98,7 +97,7 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
binding.llUploadContent.visibility = View.GONE binding.llUploadContent.visibility = View.GONE
} }
setupNewContentCreator() setupRecommendSeries()
setupBanner() setupBanner()
setupOrderList() setupOrderList()
setupNewContentTheme() setupNewContentTheme()
@ -107,17 +106,6 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
setupContentRanking() setupContentRanking()
setupCuration() setupCuration()
binding.swipeRefreshLayout.setOnRefreshListener {
binding.swipeRefreshLayout.isRefreshing = false
curationViewModel.refresh()
bannerViewModel.getMainBannerList()
newContentViewModel.getThemeList()
newContentViewModel.getNewContentOfTheme("전체")
contentRankingViewModel.getContentRanking()
contentRankingViewModel.getContentRankingSortType()
newContentCreatorViewModel.getNewContentUploadCreatorList()
}
binding.llShortPlay.setOnClickListener { binding.llShortPlay.setOnClickListener {
startActivity( startActivity(
Intent(requireContext(), AudioContentAllByThemeActivity::class.java).apply { Intent(requireContext(), AudioContentAllByThemeActivity::class.java).apply {
@ -135,20 +123,33 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
} }
} }
private fun setupNewContentCreator() { private fun setupRecommendSeries() {
newContentCreatorAdapter = AudioContentMainNewContentCreatorAdapter { seriesAdapter = UserProfileSeriesListAdapter(
val intent = Intent(requireContext(), UserProfileActivity::class.java) onClickItem = {
intent.putExtra(Constants.EXTRA_USER_ID, it) startActivity(
startActivity(intent) Intent(requireContext(), SeriesDetailActivity::class.java).apply {
} putExtra(Constants.EXTRA_SERIES_ID, it)
}
)
},
onClickCreator = {
startActivity(
Intent(requireContext(), UserProfileActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, it)
}
)
},
isVisibleCreator = true
)
binding.rvNewContentCreator.layoutManager = LinearLayoutManager( val recyclerView = binding.rvRecommendSeries
context, recyclerView.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL, LinearLayoutManager.HORIZONTAL,
false false
) )
binding.rvNewContentCreator.addItemDecoration(object : RecyclerView.ItemDecoration() { recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets( override fun getItemOffsets(
outRect: Rect, outRect: Rect,
view: View, view: View,
@ -160,28 +161,28 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
when (parent.getChildAdapterPosition(view)) { when (parent.getChildAdapterPosition(view)) {
0 -> { 0 -> {
outRect.left = 0 outRect.left = 0
outRect.right = 10.7f.dpToPx().toInt() outRect.right = 6.7f.dpToPx().toInt()
} }
newContentCreatorAdapter.itemCount - 1 -> { seriesAdapter.itemCount - 1 -> {
outRect.left = 10.7f.dpToPx().toInt()
outRect.right = 0 outRect.right = 0
outRect.left = 6.7f.dpToPx().toInt()
} }
else -> { else -> {
outRect.left = 10.7f.dpToPx().toInt() outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 10.7f.dpToPx().toInt() outRect.right = 6.7f.dpToPx().toInt()
} }
} }
} }
}) })
binding.rvNewContentCreator.adapter = newContentCreatorAdapter recyclerView.adapter = seriesAdapter
newContentCreatorViewModel.newContentUploadCreatorListLiveData.observe(viewLifecycleOwner) { recommendSeriesViewModel.seriesListLiveData.observe(viewLifecycleOwner) {
newContentCreatorAdapter.addItems(it) seriesAdapter.addItems(it)
binding.rvNewContentCreator.visibility = if ( binding.llRecommendSeries.visibility = if (
newContentCreatorAdapter.itemCount <= 0 && it.isEmpty() seriesAdapter.itemCount <= 0 && it.isEmpty()
) { ) {
View.GONE View.GONE
} else { } else {
@ -189,9 +190,14 @@ class AudioContentMainFragment : BaseFragment<FragmentAudioContentMainBinding>(
} }
} }
newContentCreatorViewModel.toastLiveData.observe(viewLifecycleOwner) { recommendSeriesViewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() } it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() }
} }
binding.llRecommendSeriesRefresh.setOnClickListener {
seriesAdapter.clear()
recommendSeriesViewModel.getRecommendSeriesList()
}
} }
private fun setupBanner() { private fun setupBanner() {

View File

@ -61,6 +61,7 @@ class AudioContentMainNewContentViewModel(
} }
fun getNewContentOfTheme(theme: String) { fun getNewContentOfTheme(theme: String) {
_isLoading.value = true
compositeDisposable.add( compositeDisposable.add(
repository.getNewContentOfTheme( repository.getNewContentOfTheme(
theme = if (theme == "전체") { theme = if (theme == "전체") {
@ -85,10 +86,13 @@ class AudioContentMainNewContentViewModel(
) )
} }
} }
_isLoading.value = false
}, },
{ {
it.message?.let { message -> Logger.e(message) } it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
_isLoading.value = false
} }
) )
) )

View File

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

View File

@ -1,44 +1,42 @@
package kr.co.vividnext.sodalive.audio_content.main.new_content_upload_creator package kr.co.vividnext.sodalive.audio_content.main.recommend_series
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.orhanobut.logger.Logger import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.audio_content.AudioContentRepository import kr.co.vividnext.sodalive.audio_content.series.GetSeriesListResponse
import kr.co.vividnext.sodalive.audio_content.main.GetNewContentUploadCreator import kr.co.vividnext.sodalive.audio_content.series.SeriesRepository
import kr.co.vividnext.sodalive.base.BaseViewModel import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.common.SharedPreferenceManager
class AudioContentMainNewContentCreatorViewModel( class AudioContentMainRecommendSeriesViewModel(
private val repository: AudioContentRepository private val repository: SeriesRepository
) : BaseViewModel() { ) : BaseViewModel() {
private val _toastLiveData = MutableLiveData<String?>() private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?> val toastLiveData: LiveData<String?>
get() = _toastLiveData get() = _toastLiveData
private var _newContentUploadCreatorListLiveData = private var _seriesListLiveData = MutableLiveData<List<GetSeriesListResponse.SeriesListItem>>()
MutableLiveData<List<GetNewContentUploadCreator>>() val seriesListLiveData: LiveData<List<GetSeriesListResponse.SeriesListItem>>
val newContentUploadCreatorListLiveData: LiveData<List<GetNewContentUploadCreator>> get() = _seriesListLiveData
get() = _newContentUploadCreatorListLiveData
fun getNewContentUploadCreatorList() { fun getRecommendSeriesList() {
compositeDisposable.add( compositeDisposable.add(
repository.getNewContentUploadCreatorList( repository
token = "Bearer ${SharedPreferenceManager.token}" .getRecommendSeriesList(token = "Bearer ${SharedPreferenceManager.token}")
)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe( .subscribe(
{ {
if (it.success && it.data != null) { if (it.success && it.data != null) {
_newContentUploadCreatorListLiveData.postValue(it.data!!) _seriesListLiveData.value = it.data!!
} else { } else {
if (it.message != null) { if (it.message != null) {
_toastLiveData.postValue(it.message) _toastLiveData.postValue(it.message)
} else { } else {
_toastLiveData.postValue( _toastLiveData.postValue(
"크리에이터 리스트를 불러오지 못했습니다. 다시 시도해 주세요.\n" + "추천 시리즈를 불러오지 못했습니다. 다시 시도해 주세요.\n" +
"계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." "계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
) )
} }
@ -47,7 +45,7 @@ class AudioContentMainNewContentCreatorViewModel(
{ {
it.message?.let { message -> Logger.e(message) } it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue( _toastLiveData.postValue(
"크리에이터 리스트를 불러오지 못했습니다. 다시 시도해 주세요.\n" + "추천 시리즈를 불러오지 못했습니다. 다시 시도해 주세요.\n" +
"계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." "계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
) )
} }

View File

@ -173,17 +173,17 @@ class AudioContentModifyActivity : BaseActivity<ActivityAudioContentModifyBindin
R.color.color_eeeeee R.color.color_eeeeee
) )
) )
binding.llCommentYes.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff) binding.llCommentYes.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.ivCommentNo.visibility = View.GONE binding.ivCommentNo.visibility = View.GONE
binding.tvCommentNo.setTextColor( binding.tvCommentNo.setTextColor(
ContextCompat.getColor( ContextCompat.getColor(
applicationContext, applicationContext,
R.color.color_9970ff R.color.color_3bb9f1
) )
) )
binding.llCommentNo.setBackgroundResource( binding.llCommentNo.setBackgroundResource(
R.drawable.bg_round_corner_6_7_1f1734_9970ff R.drawable.bg_round_corner_6_7_13181b_3bb9f1
) )
} else { } else {
binding.ivCommentNo.visibility = View.VISIBLE binding.ivCommentNo.visibility = View.VISIBLE
@ -193,17 +193,17 @@ class AudioContentModifyActivity : BaseActivity<ActivityAudioContentModifyBindin
R.color.color_eeeeee R.color.color_eeeeee
) )
) )
binding.llCommentNo.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff) binding.llCommentNo.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.ivCommentYes.visibility = View.GONE binding.ivCommentYes.visibility = View.GONE
binding.tvCommentYes.setTextColor( binding.tvCommentYes.setTextColor(
ContextCompat.getColor( ContextCompat.getColor(
applicationContext, applicationContext,
R.color.color_9970ff R.color.color_3bb9f1
) )
) )
binding.llCommentYes binding.llCommentYes
.setBackgroundResource(R.drawable.bg_round_corner_6_7_1f1734_9970ff) .setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b_3bb9f1)
} }
} }
@ -223,17 +223,17 @@ class AudioContentModifyActivity : BaseActivity<ActivityAudioContentModifyBindin
if (it) { if (it) {
binding.ivAgeAll.visibility = View.GONE binding.ivAgeAll.visibility = View.GONE
binding.llAgeAll.setBackgroundResource( binding.llAgeAll.setBackgroundResource(
R.drawable.bg_round_corner_6_7_1f1734 R.drawable.bg_round_corner_6_7_13181b
) )
binding.tvAgeAll.setTextColor( binding.tvAgeAll.setTextColor(
ContextCompat.getColor( ContextCompat.getColor(
applicationContext, applicationContext,
R.color.color_9970ff R.color.color_3bb9f1
) )
) )
binding.ivAge19.visibility = View.VISIBLE binding.ivAge19.visibility = View.VISIBLE
binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_9970ff) binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.tvAge19.setTextColor( binding.tvAge19.setTextColor(
ContextCompat.getColor( ContextCompat.getColor(
applicationContext, applicationContext,
@ -242,17 +242,17 @@ class AudioContentModifyActivity : BaseActivity<ActivityAudioContentModifyBindin
) )
} else { } else {
binding.ivAge19.visibility = View.GONE binding.ivAge19.visibility = View.GONE
binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_1f1734) binding.llAge19.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b)
binding.tvAge19.setTextColor( binding.tvAge19.setTextColor(
ContextCompat.getColor( ContextCompat.getColor(
applicationContext, applicationContext,
R.color.color_9970ff R.color.color_3bb9f1
) )
) )
binding.ivAgeAll.visibility = View.VISIBLE binding.ivAgeAll.visibility = View.VISIBLE
binding.llAgeAll.setBackgroundResource( binding.llAgeAll.setBackgroundResource(
R.drawable.bg_round_corner_6_7_9970ff R.drawable.bg_round_corner_6_7_3bb9f1
) )
binding.tvAgeAll.setTextColor( binding.tvAgeAll.setTextColor(
ContextCompat.getColor( ContextCompat.getColor(

View File

@ -4,14 +4,17 @@ import android.app.Activity
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.WindowManager import android.view.WindowManager
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import coil.load import coil.load
import coil.transform.CircleCropTransformation import coil.transform.CircleCropTransformation
import coil.transform.RoundedCornersTransformation import coil.transform.RoundedCornersTransformation
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.DialogAudioContentOrderConfirmBinding import kr.co.vividnext.sodalive.databinding.DialogAudioContentOrderConfirmBinding
import kr.co.vividnext.sodalive.extensions.dpToPx import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.moneyFormat
import kotlin.math.ceil import kotlin.math.ceil
class AudioContentOrderConfirmDialog( class AudioContentOrderConfirmDialog(
@ -58,16 +61,35 @@ class AudioContentOrderConfirmDialog(
} }
dialogView.tvDuration.text = duration dialogView.tvDuration.text = duration
dialogView.tvPrice.text = if (orderType == OrderType.RENTAL && !isOnlyRental) {
"${ceil(price * 0.6).toInt()}" if (SharedPreferenceManager.userId == 17958L) {
dialogView.ivCan.visibility = View.GONE
dialogView.tvPrice.text = if (orderType == OrderType.RENTAL && !isOnlyRental) {
"${(ceil(price * 0.6).toInt() * 110).moneyFormat()}"
} else {
"${(price * 110).moneyFormat()}"
}
} else { } else {
"$price" dialogView.ivCan.visibility = View.VISIBLE
dialogView.tvPrice.text = if (orderType == OrderType.RENTAL && !isOnlyRental) {
ceil(price * 0.6).toInt().moneyFormat()
} else {
price.moneyFormat()
}
} }
dialogView.tvNotice.text = if (orderType == OrderType.RENTAL) { if (SharedPreferenceManager.userId == 17958L) {
"콘텐츠를 대여하시겠습니까?\n아래 캔이 차감됩니다." dialogView.tvNotice.text = if (orderType == OrderType.RENTAL) {
"콘텐츠를 대여하시겠습니까?"
} else {
"콘텐츠를 소장하시겠습니까?"
}
} else { } else {
"콘텐츠를 소장하시겠습니까?\n아래 캔이 차감됩니다." dialogView.tvNotice.text = if (orderType == OrderType.RENTAL) {
"콘텐츠를 대여하시겠습니까?\n아래 캔이 차감됩니다."
} else {
"콘텐츠를 소장하시겠습니까?\n아래 캔이 차감됩니다."
}
} }
dialogView.tvCancel.setOnClickListener { dialogView.tvCancel.setOnClickListener {

View File

@ -1,11 +1,14 @@
package kr.co.vividnext.sodalive.audio_content.order package kr.co.vividnext.sodalive.audio_content.order
import android.annotation.SuppressLint
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.FragmentAudioContentOrderBinding import kr.co.vividnext.sodalive.databinding.FragmentAudioContentOrderBinding
import kr.co.vividnext.sodalive.extensions.moneyFormat
import kotlin.math.ceil import kotlin.math.ceil
class AudioContentOrderFragment( class AudioContentOrderFragment(
@ -26,15 +29,33 @@ class AudioContentOrderFragment(
return binding.root return binding.root
} }
@SuppressLint("SetTextI18n")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
if (SharedPreferenceManager.userId == 17958L) {
binding.ivKeepCan.visibility = View.GONE
binding.ivRentalCan.visibility = View.GONE
} else {
binding.ivKeepCan.visibility = View.VISIBLE
binding.ivRentalCan.visibility = View.VISIBLE
}
if (isOnlyRental) { if (isOnlyRental) {
binding.tvRental.text = "$price" if (SharedPreferenceManager.userId == 17958L) {
binding.tvRental.text = "${(price * 110).moneyFormat()}"
} else {
binding.tvRental.text = price.moneyFormat()
}
binding.rlKeep.visibility = View.GONE binding.rlKeep.visibility = View.GONE
} else { } else {
binding.tvKeep.text = "$price" if (SharedPreferenceManager.userId == 17958L) {
binding.tvRental.text = "${ceil(price * 0.6).toInt()}" binding.tvKeep.text = "${(price * 110).moneyFormat()}"
binding.tvRental.text = "${(ceil(price * 0.6).toInt() * 110).moneyFormat()}"
} else {
binding.tvKeep.text = price.moneyFormat()
binding.tvRental.text = ceil(price * 0.6).toInt().moneyFormat()
}
binding.rlKeep.visibility = View.VISIBLE binding.rlKeep.visibility = View.VISIBLE
binding.llKeep.setOnClickListener { binding.llKeep.setOnClickListener {

View File

@ -0,0 +1,26 @@
package kr.co.vividnext.sodalive.audio_content.series
import com.google.gson.annotations.SerializedName
data class GetSeriesListResponse(
@SerializedName("totalCount") val totalCount: Int,
@SerializedName("items") val items: List<SeriesListItem>
) {
data class SeriesListItem(
@SerializedName("seriesId") val seriesId: Long,
@SerializedName("title") val title: String,
@SerializedName("coverImage") val coverImage: String,
@SerializedName("publishedDaysOfWeek") val publishedDaysOfWeek: String,
@SerializedName("isComplete") val isComplete: Boolean,
@SerializedName("creator") val creator: SeriesListItemCreator,
@SerializedName("numberOfContent") val numberOfContent: Int,
@SerializedName("isNew") val isNew: Boolean,
@SerializedName("isPopular") val isPopular: Boolean
)
data class SeriesListItemCreator(
@SerializedName("creatorId") val creatorId: Long,
@SerializedName("nickname") val nickname: String,
@SerializedName("profileImage") val profileImage: String
)
}

View File

@ -0,0 +1,40 @@
package kr.co.vividnext.sodalive.audio_content.series
import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.audio_content.series.detail.GetSeriesContentListResponse
import kr.co.vividnext.sodalive.audio_content.series.detail.GetSeriesDetailResponse
import kr.co.vividnext.sodalive.common.ApiResponse
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Path
import retrofit2.http.Query
interface SeriesApi {
@GET("/audio-content/series")
fun getSeriesList(
@Query("creatorId") creatorId: Long,
@Query("sortType") sortType: SeriesListAllViewModel.SeriesSortType,
@Query("page") page: Int,
@Query("size") size: Int,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetSeriesListResponse>>
@GET("/audio-content/series/{id}")
fun getSeriesDetail(
@Path("id") seriesId: Long,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetSeriesDetailResponse>>
@GET("/audio-content/series/{id}/content")
fun getSeriesContentList(
@Path("id") seriesId: Long,
@Query("page") page: Int,
@Query("size") size: Int,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetSeriesContentListResponse>>
@GET("/audio-content/series/recommend")
fun getRecommendSeriesList(
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetSeriesListResponse.SeriesListItem>>>
}

View File

@ -0,0 +1,104 @@
package kr.co.vividnext.sodalive.audio_content.series
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
import coil.transform.RoundedCornersTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemSeriesListBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
class SeriesListAdapter(
private val itemWidth: Int,
private val onClickItem: (Long) -> Unit,
private val onClickCreator: (Long) -> Unit,
private val isVisibleCreator: Boolean
) : RecyclerView.Adapter<SeriesListAdapter.ViewHolder>() {
val items = mutableListOf<GetSeriesListResponse.SeriesListItem>()
inner class ViewHolder(
private val binding: ItemSeriesListBinding
) : RecyclerView.ViewHolder(binding.root) {
@SuppressLint("SetTextI18n")
fun bind(item: GetSeriesListResponse.SeriesListItem) {
val lp = binding.ivCover.layoutParams as ConstraintLayout.LayoutParams
lp.width = itemWidth
lp.height = itemWidth * 432 / 306
binding.ivCover.layoutParams = lp
binding.ivCover.load(item.coverImage) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(RoundedCornersTransformation(5f.dpToPx()))
}
binding.tvTitle.text = item.title
binding.tvSeriesContentCount.text = "${item.numberOfContent}"
binding.tvPublishedDaysOfWeek.text = item.publishedDaysOfWeek
binding.tvNew.visibility = if (item.isNew) {
View.VISIBLE
} else {
View.GONE
}
binding.tvPopular.visibility = if (item.isPopular) {
View.VISIBLE
} else {
View.GONE
}
if (item.isComplete) {
binding.tvNew.visibility = View.GONE
binding.tvComplete.visibility = View.VISIBLE
} else {
binding.tvComplete.visibility = View.GONE
}
if (isVisibleCreator) {
binding.llCreator.visibility = View.VISIBLE
binding.tvCreator.text = item.creator.nickname
binding.ivCreator.load(item.creator.profileImage) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}
binding.llCreator.setOnClickListener { onClickCreator(item.creator.creatorId) }
} else {
binding.llCreator.visibility = View.GONE
}
binding.root.setOnClickListener { onClickItem(item.seriesId) }
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
ItemSeriesListBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
}
override fun getItemCount() = items.count()
@SuppressLint("NotifyDataSetChanged")
fun addItems(items: List<GetSeriesListResponse.SeriesListItem>) {
this.items.addAll(items)
notifyDataSetChanged()
}
fun clear() {
this.items.clear()
}
}

View File

@ -0,0 +1,118 @@
package kr.co.vividnext.sodalive.audio_content.series
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.DifferentSpacingItemDecoration
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.ActivitySeriesListAllBinding
import org.koin.android.ext.android.inject
import kotlin.math.roundToInt
class SeriesListAllActivity : BaseActivity<ActivitySeriesListAllBinding>(
ActivitySeriesListAllBinding::inflate
) {
private val viewModel: SeriesListAllViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var seriesAdapter: SeriesListAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val creatorId = intent.getLongExtra(Constants.EXTRA_USER_ID, 0)
if (creatorId <= 0) {
Toast.makeText(applicationContext, "잘못된 요청입니다.", Toast.LENGTH_LONG).show()
finish()
}
bindData()
viewModel.creatorId = creatorId
viewModel.getSeriesList()
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.text = "시리즈 전체보기"
binding.toolbar.tvBack.setOnClickListener { finish() }
seriesAdapter = SeriesListAdapter(
itemWidth = ((screenWidth - (13.3 * 3)) / 3).roundToInt(),
onClickItem = {
startActivity(
Intent(applicationContext, SeriesDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_SERIES_ID, it)
}
)
},
onClickCreator = {},
isVisibleCreator = false
)
val spanCount = 3
val horizontalSpacing = 20
val verticalSpacing = 100
val recyclerView = binding.rvSeriesAll
recyclerView.layoutManager = GridLayoutManager(this, spanCount)
recyclerView.addItemDecoration(
DifferentSpacingItemDecoration(
spanCount = spanCount,
horizontalSpacing = horizontalSpacing,
verticalSpacing = verticalSpacing,
includeEdge = true
)
)
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.getSeriesList()
}
}
})
recyclerView.adapter = seriesAdapter
}
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.seriesListLiveData.observe(this) {
if (viewModel.page - 1 == 1) {
seriesAdapter.clear()
binding.rvSeriesAll.scrollToPosition(0)
}
seriesAdapter.addItems(it)
}
}
}

View File

@ -0,0 +1,80 @@
package kr.co.vividnext.sodalive.audio_content.series
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
class SeriesListAllViewModel(private val repository: SeriesRepository) : BaseViewModel() {
enum class SeriesSortType {
@SerializedName("NEWEST") NEWEST,
@SerializedName("POPULAR") POPULAR
}
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
private var _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean>
get() = _isLoading
private var _seriesListLiveData = MutableLiveData<List<GetSeriesListResponse.SeriesListItem>>()
val seriesListLiveData: LiveData<List<GetSeriesListResponse.SeriesListItem>>
get() = _seriesListLiveData
var creatorId = 0L
var isLast = false
var page = 1
private val size = 10
fun getSeriesList() {
if (!_isLoading.value!! && !isLast) {
_isLoading.value = true
compositeDisposable.add(
repository.getSeriesList(
creatorId = creatorId,
sortType = SeriesSortType.NEWEST,
page = page,
size = size,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
page += 1
if (it.data.items.isNotEmpty()) {
_seriesListLiveData.value = it.data.items
} else {
isLast = true
}
} else {
if (it.message != null) {
_toastLiveData.value = it.message
} else {
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
}
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
}
}

View File

@ -0,0 +1,36 @@
package kr.co.vividnext.sodalive.audio_content.series
class SeriesRepository(private val api: SeriesApi) {
fun getSeriesList(
creatorId: Long,
sortType: SeriesListAllViewModel.SeriesSortType,
page: Int,
size: Int,
token: String
) = api.getSeriesList(
creatorId = creatorId,
sortType = sortType,
page = page - 1,
size = size,
authHeader = token
)
fun getSeriesDetail(seriesId: Long, token: String) = api.getSeriesDetail(
seriesId = seriesId,
authHeader = token
)
fun getSeriesContentList(
seriesId: Long,
page: Int,
size: Int,
token: String
) = api.getSeriesContentList(
seriesId = seriesId,
page = page - 1,
size = size,
authHeader = token
)
fun getRecommendSeriesList(token: String) = api.getRecommendSeriesList(authHeader = token)
}

View File

@ -0,0 +1,77 @@
package kr.co.vividnext.sodalive.audio_content.series.content
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.RoundedCornersTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.series.detail.GetSeriesContentListItem
import kr.co.vividnext.sodalive.databinding.ItemSeriesContentBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
class SeriesContentAdapter(
private val onClickItem: (Long) -> Unit
) : RecyclerView.Adapter<SeriesContentAdapter.ViewHolder>() {
val items = mutableListOf<GetSeriesContentListItem>()
inner class ViewHolder(
private val binding: ItemSeriesContentBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetSeriesContentListItem) {
binding.ivCover.load(item.coverImage) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(RoundedCornersTransformation(5.3f.dpToPx()))
}
binding.tvTitle.text = item.title
binding.tvDuration.text = item.duration
binding.tvPrice.visibility = View.GONE
binding.tvOwned.visibility = View.GONE
binding.tvRented.visibility = View.GONE
binding.tvPriceFree.visibility = View.GONE
if (item.isOwned) {
binding.tvOwned.visibility = View.VISIBLE
} else if (item.isRented) {
binding.tvRented.visibility = View.VISIBLE
} else if (item.price > 0) {
binding.tvPrice.text = "${item.price}"
binding.tvPrice.visibility = View.VISIBLE
} else {
binding.tvPriceFree.visibility = View.VISIBLE
}
binding.root.setOnClickListener { onClickItem(item.contentId) }
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
ItemSeriesContentBinding.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<GetSeriesContentListItem>) {
this.items.addAll(items)
notifyDataSetChanged()
}
fun clear() {
this.items.clear()
}
}

View File

@ -0,0 +1,108 @@
package kr.co.vividnext.sodalive.audio_content.series.content
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.recyclerview.widget.DividerItemDecoration
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.ActivitySeriesContentAllBinding
import org.koin.android.ext.android.inject
class SeriesContentAllActivity : BaseActivity<ActivitySeriesContentAllBinding>(
ActivitySeriesContentAllBinding::inflate
) {
private val viewModel: SeriesContentAllViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var adapter: SeriesContentAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val seriesId = intent.getLongExtra(Constants.EXTRA_SERIES_ID, 0)
if (seriesId <= 0) {
Toast.makeText(applicationContext, "잘못된 요청입니다.", Toast.LENGTH_LONG).show()
finish()
}
bindData()
viewModel.seriesId = seriesId
viewModel.getSeriesContentList()
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
val seriesTitle = intent.getStringExtra(Constants.EXTRA_SERIES_TITLE) ?: ""
binding.toolbar.tvBack.text = if (seriesTitle.isNotBlank()) {
"$seriesTitle - 전체회차 듣기"
} else {
" 전체회차 듣기"
}
binding.toolbar.tvBack.setOnClickListener { finish() }
adapter = SeriesContentAdapter {
startActivity(
Intent(applicationContext, AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
}
)
}
binding.rvSeriesContentAll.layoutManager = LinearLayoutManager(
applicationContext,
LinearLayoutManager.VERTICAL,
false
)
binding.rvSeriesContentAll.addItemDecoration(
DividerItemDecoration(
applicationContext,
DividerItemDecoration.VERTICAL
)
)
binding.rvSeriesContentAll.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.getSeriesContentList()
}
}
})
binding.rvSeriesContentAll.adapter = adapter
}
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.seriesContentListLiveData.observe(this) {
adapter.addItems(it)
}
}
}

View File

@ -0,0 +1,73 @@
package kr.co.vividnext.sodalive.audio_content.series.content
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.audio_content.series.SeriesRepository
import kr.co.vividnext.sodalive.audio_content.series.detail.GetSeriesContentListItem
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
class SeriesContentAllViewModel(private val repository: SeriesRepository) : 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 _seriesContentListLiveData = MutableLiveData<List<GetSeriesContentListItem>>()
val seriesContentListLiveData: LiveData<List<GetSeriesContentListItem>>
get() = _seriesContentListLiveData
var seriesId = 0L
var page = 1
private var pageSize = 10
private var isLast = false
fun getSeriesContentList() {
if (!_isLoading.value!! && !isLast) {
_isLoading.value = true
compositeDisposable.add(
repository.getSeriesContentList(
seriesId = seriesId,
page = page,
size = pageSize,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
page += 1
if (it.data.items.isNotEmpty()) {
_seriesContentListLiveData.value = it.data.items
} else {
isLast = true
}
} else {
if (it.message != null) {
_toastLiveData.value = it.message
} else {
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
}
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
}
}

View File

@ -0,0 +1,22 @@
package kr.co.vividnext.sodalive.audio_content.series.detail
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
data class GetSeriesContentListResponse(
@SerializedName("totalCount") val totalCount: Int,
@SerializedName("items") val items: List<GetSeriesContentListItem>
)
@Parcelize
data class GetSeriesContentListItem(
@SerializedName("contentId") val contentId: Long,
@SerializedName("title") val title: String,
@SerializedName("coverImage") val coverImage: String,
@SerializedName("releaseDate") val releaseDate: String,
@SerializedName("duration") val duration: String,
@SerializedName("price") val price: Int,
@SerializedName("isRented") var isRented: Boolean,
@SerializedName("isOwned") var isOwned: Boolean
) : Parcelable

View File

@ -0,0 +1,36 @@
package kr.co.vividnext.sodalive.audio_content.series.detail
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
@Parcelize
data class GetSeriesDetailResponse(
@SerializedName("seriesId") val seriesId: Long,
@SerializedName("title") val title: String,
@SerializedName("coverImage") val coverImage: String,
@SerializedName("introduction") val introduction: String,
@SerializedName("genre") val genre: String,
@SerializedName("isAdult") val isAdult: Boolean,
@SerializedName("writer") val writer: String?,
@SerializedName("studio") val studio: String?,
@SerializedName("publishedDate") val publishedDate: String,
@SerializedName("creator") val creator: GetSeriesDetailCreator,
@SerializedName("rentalMinPrice") var rentalMinPrice: Int,
@SerializedName("rentalMaxPrice") var rentalMaxPrice: Int,
@SerializedName("rentalPeriod") val rentalPeriod: Int,
@SerializedName("minPrice") var minPrice: Int,
@SerializedName("maxPrice") var maxPrice: Int,
@SerializedName("keywordList") var keywordList: List<String>,
@SerializedName("publishedDaysOfWeek") var publishedDaysOfWeek: String,
@SerializedName("contentList") val contentList: List<GetSeriesContentListItem>,
@SerializedName("contentCount") val contentCount: Int
) : Parcelable {
@Parcelize
data class GetSeriesDetailCreator(
@SerializedName("creatorId") val creatorId: Long,
@SerializedName("nickname") val nickname: String,
@SerializedName("profileImage") val profileImage: String,
@SerializedName("isFollow") var isFollow: Boolean
) : Parcelable
}

View File

@ -0,0 +1,212 @@
package kr.co.vividnext.sodalive.audio_content.series.detail
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import coil.load
import coil.size.Scale
import coil.transform.BlurTransformation
import coil.transform.CircleCropTransformation
import coil.transform.RoundedCornersTransformation
import com.google.android.material.chip.Chip
import com.google.android.material.shape.ShapeAppearanceModel
import com.google.android.material.tabs.TabLayout
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.ActivitySeriesDetailBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
class SeriesDetailActivity : BaseActivity<ActivitySeriesDetailBinding>(
ActivitySeriesDetailBinding::inflate
) {
private val viewModel: SeriesDetailViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val seriesId = intent.getLongExtra(Constants.EXTRA_SERIES_ID, 0)
if (seriesId <= 0) {
Toast.makeText(applicationContext, "잘못된 요청입니다.", Toast.LENGTH_LONG).show()
finish()
}
bindData()
viewModel.seriesId = seriesId
viewModel.getSeriesDetail()
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.ivBack.setOnClickListener { finish() }
setupTab()
}
private fun setupTab() {
val tabs = binding.tabs
tabs.addTab(tabs.newTab().setText("").setTag("home"))
tabs.addTab(tabs.newTab().setText("작품소개").setTag("introduction"))
tabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {
val tag = tab.tag as String
changeFragment(tag)
}
override fun onTabUnselected(tab: TabLayout.Tab) {
}
override fun onTabReselected(tab: TabLayout.Tab) {
}
})
}
private fun changeFragment(tag: String) {
val fragmentManager = supportFragmentManager
val fragmentTransaction = fragmentManager.beginTransaction()
val fragment = when (tag) {
"introduction" -> SeriesDetailIntroductionFragment()
else -> SeriesDetailHomeFragment()
}
val bundle = Bundle()
bundle.putParcelable(Constants.EXTRA_SERIES, viewModel.seriesDetailResponse)
fragment.arguments = bundle
fragmentTransaction.replace(R.id.container, fragment, tag)
fragmentTransaction.setPrimaryNavigationFragment(fragment)
fragmentTransaction.setReorderingAllowed(true)
fragmentTransaction.commitNow()
}
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.seriesDetailLiveData.observe(this) {
setSeriesBg(it.coverImage)
setSeriesInfo(it)
setSeriesCreator(it.creator)
setSeriesKeywordChipList(it.keywordList)
changeFragment("home")
}
}
private fun setSeriesKeywordChipList(keywordList: List<String>) {
binding.chipGroup.isSingleLine = true
binding.chipGroup.isHorizontalScrollBarEnabled = false
for (keyword in keywordList) {
val chip = Chip(this)
chip.text = keyword
chip.isClickable = false
chip.isCheckable = false
chip.textSize = 12f
chip.chipStrokeWidth = 0f
chip.setChipBackgroundColorResource(R.color.color_222222)
val shapeAppearanceModel = ShapeAppearanceModel.Builder()
.setAllCornerSizes(26.7f.dpToPx())
.build()
chip.shapeAppearanceModel = shapeAppearanceModel
chip.setPadding(0, 0, 0, 0)
chip.setTextColor(ContextCompat.getColor(applicationContext, R.color.color_d2d2d2))
chip.typeface = ResourcesCompat.getFont(applicationContext, R.font.gmarket_sans_medium)
binding.chipGroup.addView(chip)
}
}
@SuppressLint("SetTextI18n")
private fun setSeriesInfo(seriesDetail: GetSeriesDetailResponse) {
binding.ivCover.load(seriesDetail.coverImage) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(RoundedCornersTransformation(5f.dpToPx()))
}
binding.tvTitle.text = seriesDetail.title
binding.tvGenre.text = seriesDetail.genre
binding.tvPublishedDaysOfWeek.text = "${seriesDetail.publishedDaysOfWeek} 연재"
if (seriesDetail.isAdult) {
binding.tvAge19.visibility = View.VISIBLE
binding.tvAgeAll.visibility = View.GONE
} else {
binding.tvAge19.visibility = View.GONE
binding.tvAgeAll.visibility = View.VISIBLE
}
}
private fun setSeriesCreator(creator: GetSeriesDetailResponse.GetSeriesDetailCreator) {
binding.tvNickname.text = creator.nickname
binding.ivProfile.load(creator.profileImage) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(CircleCropTransformation())
}
if (SharedPreferenceManager.userId != creator.creatorId) {
binding.ivFollow.visibility = View.VISIBLE
binding.ivFollow.setImageResource(
if (creator.isFollow) {
R.drawable.btn_following_big
} else {
R.drawable.btn_follow_big
}
)
} else {
binding.ivFollow.visibility = View.GONE
}
binding.ivFollow.setOnClickListener {
if (creator.isFollow) {
viewModel.unFollow(creator.creatorId) {
creator.isFollow = false
binding.ivFollow.setImageResource(R.drawable.btn_follow_big)
}
} else {
viewModel.follow(creator.creatorId) {
creator.isFollow = true
binding.ivFollow.setImageResource(R.drawable.btn_following_big)
}
}
}
}
private fun setSeriesBg(coverImage: String) {
binding.ivBg.load(coverImage) {
transformations(
BlurTransformation(
this@SeriesDetailActivity,
25f,
2.5f
)
)
scale(Scale.FILL)
}
}
}

View File

@ -0,0 +1,83 @@
package kr.co.vividnext.sodalive.audio_content.series.detail
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.view.View
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.series.content.SeriesContentAdapter
import kr.co.vividnext.sodalive.audio_content.series.content.SeriesContentAllActivity
import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.databinding.FragmentSeriesDetailHomeBinding
class SeriesDetailHomeFragment : BaseFragment<FragmentSeriesDetailHomeBinding>(
FragmentSeriesDetailHomeBinding::inflate
) {
private var seriesDetailResponse: GetSeriesDetailResponse? = null
private lateinit var adapter: SeriesContentAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (arguments != null) {
seriesDetailResponse = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
requireArguments().getParcelable(
Constants.EXTRA_SERIES,
GetSeriesDetailResponse::class.java
)
} else {
requireArguments().getParcelable(Constants.EXTRA_SERIES)
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (seriesDetailResponse != null) {
setContent()
}
}
@SuppressLint("SetTextI18n")
private fun setContent() {
binding.tvTotalCount.text = "(${seriesDetailResponse!!.contentCount})"
binding.llContentAll.setOnClickListener {
startActivity(
Intent(requireActivity(), SeriesContentAllActivity::class.java).apply {
putExtra(Constants.EXTRA_SERIES_ID, seriesDetailResponse!!.seriesId)
putExtra(Constants.EXTRA_SERIES_TITLE, seriesDetailResponse!!.title)
}
)
}
adapter = SeriesContentAdapter {
startActivity(
Intent(requireActivity(), AudioContentDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
}
)
}
binding.rvContent.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.VERTICAL,
false
)
binding.rvContent.addItemDecoration(
DividerItemDecoration(
requireContext(),
DividerItemDecoration.VERTICAL
)
)
binding.rvContent.adapter = adapter
adapter.addItems(seriesDetailResponse!!.contentList)
}
}

View File

@ -0,0 +1,142 @@
package kr.co.vividnext.sodalive.audio_content.series.detail
import android.annotation.SuppressLint
import android.os.Build
import android.os.Bundle
import android.view.View
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup
import com.google.android.material.shape.ShapeAppearanceModel
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.databinding.FragmentSeriesDetailIntroductionBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
class SeriesDetailIntroductionFragment : BaseFragment<FragmentSeriesDetailIntroductionBinding>(
FragmentSeriesDetailIntroductionBinding::inflate
) {
private var seriesDetailResponse: GetSeriesDetailResponse? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (arguments != null) {
seriesDetailResponse = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
requireArguments().getParcelable(
Constants.EXTRA_SERIES,
GetSeriesDetailResponse::class.java
)
} else {
requireArguments().getParcelable(Constants.EXTRA_SERIES)
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (seriesDetailResponse != null) {
setSeriesKeywordChipList(seriesDetailResponse!!.keywordList)
setSeriesIntroduction(seriesDetailResponse!!.introduction)
setSeriesPrice()
setSeriesInfo()
}
}
private fun setSeriesPrice() {
val rentalMinPrice = seriesDetailResponse!!.rentalMinPrice
val rentalMaxPrice = seriesDetailResponse!!.rentalMaxPrice
val minPrice = seriesDetailResponse!!.minPrice
val maxPrice = seriesDetailResponse!!.maxPrice
binding.tvRentalPrice.text = if (rentalMinPrice == rentalMaxPrice) {
if (rentalMaxPrice == 0) {
"무료(15일)"
} else {
"$rentalMaxPrice(15일)"
}
} else {
"${if (rentalMinPrice == 0) "무료" else rentalMinPrice} ~ ${rentalMaxPrice}캔 (15일)"
}
binding.tvPrice.text = if (minPrice == maxPrice) {
if (maxPrice == 0) {
"무료"
} else {
"$maxPrice"
}
} else {
"${if (minPrice == 0) "무료" else minPrice} ~ ${maxPrice}"
}
}
@SuppressLint("SetTextI18n")
private fun setSeriesInfo() {
binding.tvGenre.text = seriesDetailResponse!!.genre
binding.tvIsAdult.text = if (seriesDetailResponse!!.isAdult) {
"19세 이상"
} else {
"전체연령가"
}
binding.tvPublishedDate.text = seriesDetailResponse!!.publishedDate
binding.tvPublishedDaysOfWeek.text =
if (seriesDetailResponse!!.publishedDaysOfWeek == "랜덤") {
seriesDetailResponse!!.publishedDaysOfWeek
} else {
"${seriesDetailResponse!!.publishedDaysOfWeek}요일"
}
if (seriesDetailResponse!!.writer != null) {
binding.tvWriter.visibility = View.VISIBLE
binding.tvWriterLabel.visibility = View.VISIBLE
binding.tvWriter.text = seriesDetailResponse!!.writer
} else {
binding.tvWriter.visibility = View.GONE
binding.tvWriterLabel.visibility = View.GONE
}
if (seriesDetailResponse!!.studio != null) {
binding.tvStudio.visibility = View.VISIBLE
binding.tvStudioLabel.visibility = View.VISIBLE
binding.tvStudio.text = seriesDetailResponse!!.studio
} else {
binding.tvStudio.visibility = View.GONE
binding.tvStudioLabel.visibility = View.GONE
}
}
private fun setSeriesIntroduction(introduction: String) {
binding.tvIntroduce.text = introduction
}
private fun setSeriesKeywordChipList(keywordList: List<String>) {
binding.chipGroup.isHorizontalScrollBarEnabled = false
for (keyword in keywordList) {
val chip = Chip(requireActivity())
chip.text = keyword
chip.isClickable = false
chip.isCheckable = false
chip.textSize = 12f
chip.chipStrokeWidth = 0f
chip.setChipBackgroundColorResource(R.color.color_222222)
val shapeAppearanceModel = ShapeAppearanceModel.Builder()
.setAllCornerSizes(26.7f.dpToPx())
.build()
chip.shapeAppearanceModel = shapeAppearanceModel
chip.setEnsureMinTouchTargetSize(false)
chip.setPadding(0, 0, 0, 0)
chip.setTextColor(ContextCompat.getColor(requireContext(), R.color.color_d2d2d2))
chip.typeface = ResourcesCompat.getFont(requireContext(), R.font.gmarket_sans_medium)
binding.chipGroup.addView(chip)
}
}
}

View File

@ -0,0 +1,131 @@
package kr.co.vividnext.sodalive.audio_content.series.detail
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.audio_content.series.SeriesRepository
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.user.UserRepository
class SeriesDetailViewModel(
private val repository: SeriesRepository,
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 _seriesDetailLiveData = MutableLiveData<GetSeriesDetailResponse>()
val seriesDetailLiveData: LiveData<GetSeriesDetailResponse>
get() = _seriesDetailLiveData
var seriesId = 0L
lateinit var seriesDetailResponse: GetSeriesDetailResponse
fun getSeriesDetail() {
_isLoading.value = true
compositeDisposable.add(
repository.getSeriesDetail(
seriesId = seriesId,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
seriesDetailResponse = it.data
_seriesDetailLiveData.value = seriesDetailResponse
} else {
if (it.message != null) {
_toastLiveData.value = it.message
} else {
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
}
}
_isLoading.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
fun follow(creatorId: Long, onSuccess: () -> Unit) {
_isLoading.value = true
compositeDisposable.add(
userRepository.creatorFollow(
creatorId,
"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.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
fun unFollow(creatorId: Long, onSuccess: () -> Unit) {
_isLoading.value = true
compositeDisposable.add(
userRepository.creatorUnFollow(
creatorId,
"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.value = false
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
}

View File

@ -20,14 +20,17 @@ object Constants {
const val EXTRA_DATA = "extra_data" const val EXTRA_DATA = "extra_data"
const val EXTRA_TERMS = "extra_terms" const val EXTRA_TERMS = "extra_terms"
const val EXTRA_EVENT = "extra_event" const val EXTRA_EVENT = "extra_event"
const val EXTRA_SERIES = "extra_series"
const val EXTRA_NOTICE = "extra_notice" const val EXTRA_NOTICE = "extra_notice"
const val EXTRA_ROOM_ID = "extra_room_id" const val EXTRA_ROOM_ID = "extra_room_id"
const val EXTRA_USER_ID = "extra_user_id" const val EXTRA_USER_ID = "extra_user_id"
const val EXTRA_THEME_ID = "extra_theme_id" const val EXTRA_THEME_ID = "extra_theme_id"
const val EXTRA_SERIES_ID = "extra_series_id"
const val EXTRA_NICKNAME = "extra_nickname" const val EXTRA_NICKNAME = "extra_nickname"
const val EXTRA_MESSAGE_ID = "extra_message_id" const val EXTRA_MESSAGE_ID = "extra_message_id"
const val EXTRA_ROOM_DETAIL = "extra_room_detail" const val EXTRA_ROOM_DETAIL = "extra_room_detail"
const val EXTRA_MESSAGE_BOX = "extra_message_box" const val EXTRA_MESSAGE_BOX = "extra_message_box"
const val EXTRA_SERIES_TITLE = "extra_series_title"
const val EXTRA_TEXT_MESSAGE = "extra_text_message" const val EXTRA_TEXT_MESSAGE = "extra_text_message"
const val EXTRA_LIVE_TIME_NOW = "extra_live_time_now" const val EXTRA_LIVE_TIME_NOW = "extra_live_time_now"
const val EXTRA_RESULT_ROULETTE = "extra_result_roulette" const val EXTRA_RESULT_ROULETTE = "extra_result_roulette"

View File

@ -0,0 +1,39 @@
package kr.co.vividnext.sodalive.common
import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.RecyclerView
class DifferentSpacingItemDecoration(
private val spanCount: Int,
private val horizontalSpacing: Int,
private val verticalSpacing: Int,
private val includeEdge: Boolean
) : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
val position = parent.getChildAdapterPosition(view) // 아이템의 위치
val column = position % spanCount // 아이템의 열 위치
if (includeEdge) {
outRect.left = horizontalSpacing - column * horizontalSpacing / spanCount
outRect.right = (column + 1) * horizontalSpacing / spanCount
if (position < spanCount) {
outRect.top = verticalSpacing
}
outRect.bottom = verticalSpacing
} else {
outRect.left = column * horizontalSpacing / spanCount
outRect.right = horizontalSpacing - (column + 1) * horizontalSpacing / spanCount
if (position >= spanCount) {
outRect.top = verticalSpacing
}
}
}
}

View File

@ -19,11 +19,16 @@ import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailViewModel
import kr.co.vividnext.sodalive.audio_content.main.banner.AudioContentMainBannerViewModel import kr.co.vividnext.sodalive.audio_content.main.banner.AudioContentMainBannerViewModel
import kr.co.vividnext.sodalive.audio_content.main.curation.AudioContentMainCurationViewModel import kr.co.vividnext.sodalive.audio_content.main.curation.AudioContentMainCurationViewModel
import kr.co.vividnext.sodalive.audio_content.main.new_content.AudioContentMainNewContentViewModel import kr.co.vividnext.sodalive.audio_content.main.new_content.AudioContentMainNewContentViewModel
import kr.co.vividnext.sodalive.audio_content.main.new_content_upload_creator.AudioContentMainNewContentCreatorViewModel
import kr.co.vividnext.sodalive.audio_content.main.order.AudioContentMainOrderListViewModel import kr.co.vividnext.sodalive.audio_content.main.order.AudioContentMainOrderListViewModel
import kr.co.vividnext.sodalive.audio_content.main.ranking.AudioContentMainRankingViewModel import kr.co.vividnext.sodalive.audio_content.main.ranking.AudioContentMainRankingViewModel
import kr.co.vividnext.sodalive.audio_content.main.recommend_series.AudioContentMainRecommendSeriesViewModel
import kr.co.vividnext.sodalive.audio_content.modify.AudioContentModifyViewModel 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.order.AudioContentOrderListViewModel
import kr.co.vividnext.sodalive.audio_content.series.SeriesApi
import kr.co.vividnext.sodalive.audio_content.series.SeriesListAllViewModel
import kr.co.vividnext.sodalive.audio_content.series.SeriesRepository
import kr.co.vividnext.sodalive.audio_content.series.content.SeriesContentAllViewModel
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailViewModel
import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadViewModel import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadViewModel
import kr.co.vividnext.sodalive.audio_content.upload.theme.AudioContentThemeViewModel import kr.co.vividnext.sodalive.audio_content.upload.theme.AudioContentThemeViewModel
import kr.co.vividnext.sodalive.common.ApiBuilder import kr.co.vividnext.sodalive.common.ApiBuilder
@ -77,7 +82,10 @@ import kr.co.vividnext.sodalive.mypage.can.CanRepository
import kr.co.vividnext.sodalive.mypage.can.charge.iap.CanChargeIapViewModel import kr.co.vividnext.sodalive.mypage.can.charge.iap.CanChargeIapViewModel
import kr.co.vividnext.sodalive.mypage.can.charge.pg.CanChargePgViewModel import kr.co.vividnext.sodalive.mypage.can.charge.pg.CanChargePgViewModel
import kr.co.vividnext.sodalive.mypage.can.coupon.CanCouponViewModel import kr.co.vividnext.sodalive.mypage.can.coupon.CanCouponViewModel
import kr.co.vividnext.sodalive.mypage.can.payment.CanPaymentTempRepository
import kr.co.vividnext.sodalive.mypage.can.payment.CanPaymentTempViewModel
import kr.co.vividnext.sodalive.mypage.can.payment.CanPaymentViewModel import kr.co.vividnext.sodalive.mypage.can.payment.CanPaymentViewModel
import kr.co.vividnext.sodalive.mypage.can.payment.CanTempApi
import kr.co.vividnext.sodalive.mypage.can.status.CanStatusViewModel import kr.co.vividnext.sodalive.mypage.can.status.CanStatusViewModel
import kr.co.vividnext.sodalive.mypage.profile.ProfileUpdateViewModel import kr.co.vividnext.sodalive.mypage.profile.ProfileUpdateViewModel
import kr.co.vividnext.sodalive.mypage.profile.nickname.NicknameUpdateViewModel import kr.co.vividnext.sodalive.mypage.profile.nickname.NicknameUpdateViewModel
@ -116,6 +124,7 @@ import org.koin.dsl.module
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.adapter.rxjava3.RxJava3CallAdapterFactory import retrofit2.adapter.rxjava3.RxJava3CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
class AppDI(private val context: Context, isDebugMode: Boolean) { class AppDI(private val context: Context, isDebugMode: Boolean) {
private val baseUrl = BuildConfig.BASE_URL private val baseUrl = BuildConfig.BASE_URL
@ -138,6 +147,9 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
OkHttpClient().newBuilder() OkHttpClient().newBuilder()
.addInterceptor(logging) .addInterceptor(logging)
.authenticator(TokenAuthenticator(get())) .authenticator(TokenAuthenticator(get()))
.connectTimeout(60, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
.build() .build()
} }
@ -151,12 +163,14 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
} }
single { ApiBuilder().build(get(), CanApi::class.java) } single { ApiBuilder().build(get(), CanApi::class.java) }
single { ApiBuilder().build(get(), CanTempApi::class.java) }
single { ApiBuilder().build(get(), AuthApi::class.java) } single { ApiBuilder().build(get(), AuthApi::class.java) }
single { ApiBuilder().build(get(), UserApi::class.java) } single { ApiBuilder().build(get(), UserApi::class.java) }
single { ApiBuilder().build(get(), MenuApi::class.java) } single { ApiBuilder().build(get(), MenuApi::class.java) }
single { ApiBuilder().build(get(), LiveApi::class.java) } single { ApiBuilder().build(get(), LiveApi::class.java) }
single { ApiBuilder().build(get(), TermsApi::class.java) } single { ApiBuilder().build(get(), TermsApi::class.java) }
single { ApiBuilder().build(get(), EventApi::class.java) } single { ApiBuilder().build(get(), EventApi::class.java) }
single { ApiBuilder().build(get(), SeriesApi::class.java) }
single { ApiBuilder().build(get(), ReportApi::class.java) } single { ApiBuilder().build(get(), ReportApi::class.java) }
single { ApiBuilder().build(get(), LiveRecommendApi::class.java) } single { ApiBuilder().build(get(), LiveRecommendApi::class.java) }
single { ApiBuilder().build(get(), ExplorerApi::class.java) } single { ApiBuilder().build(get(), ExplorerApi::class.java) }
@ -181,6 +195,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
viewModel { CanStatusViewModel(get()) } viewModel { CanStatusViewModel(get()) }
viewModel { CanChargePgViewModel(get()) } viewModel { CanChargePgViewModel(get()) }
viewModel { CanPaymentViewModel(get()) } viewModel { CanPaymentViewModel(get()) }
viewModel { CanPaymentTempViewModel(get()) }
viewModel { LiveRoomDetailViewModel(get()) } viewModel { LiveRoomDetailViewModel(get()) }
viewModel { LiveRoomCreateViewModel(get()) } viewModel { LiveRoomCreateViewModel(get()) }
viewModel { LiveTagViewModel(get()) } viewModel { LiveTagViewModel(get()) }
@ -195,11 +210,14 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
viewModel { VoiceMessageViewModel(get()) } viewModel { VoiceMessageViewModel(get()) }
viewModel { VoiceMessageWriteViewModel(get()) } viewModel { VoiceMessageWriteViewModel(get()) }
viewModel { SelectMessageRecipientViewModel(get(), get()) } viewModel { SelectMessageRecipientViewModel(get(), get()) }
viewModel { SignOutViewModel(get()) }
viewModel { NoticeViewModel(get()) } viewModel { NoticeViewModel(get()) }
viewModel { EventViewModel(get()) } viewModel { EventViewModel(get()) }
viewModel { NotificationSettingsViewModel(get()) } viewModel { NotificationSettingsViewModel(get()) }
viewModel { SettingsViewModel(get()) } viewModel { SettingsViewModel(get()) }
viewModel { SeriesDetailViewModel(get(), get()) }
viewModel { SeriesListAllViewModel(get()) }
viewModel { SeriesContentAllViewModel(get()) }
viewModel { SignOutViewModel(get()) }
viewModel { TextMessageDetailViewModel(get()) } viewModel { TextMessageDetailViewModel(get()) }
viewModel { LiveReservationStatusViewModel(get()) } viewModel { LiveReservationStatusViewModel(get()) }
viewModel { AudioContentMainBannerViewModel(get()) } viewModel { AudioContentMainBannerViewModel(get()) }
@ -207,7 +225,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
viewModel { AudioContentMainCurationViewModel(get()) } viewModel { AudioContentMainCurationViewModel(get()) }
viewModel { AudioContentMainOrderListViewModel(get()) } viewModel { AudioContentMainOrderListViewModel(get()) }
viewModel { AudioContentMainNewContentViewModel(get()) } viewModel { AudioContentMainNewContentViewModel(get()) }
viewModel { AudioContentMainNewContentCreatorViewModel(get()) } viewModel { AudioContentMainRecommendSeriesViewModel(get()) }
viewModel { AudioContentViewModel(get()) } viewModel { AudioContentViewModel(get()) }
viewModel { AudioContentOrderListViewModel(get()) } viewModel { AudioContentOrderListViewModel(get()) }
viewModel { AudioContentUploadViewModel(get()) } viewModel { AudioContentUploadViewModel(get()) }
@ -238,11 +256,13 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
private val repositoryModule = module { private val repositoryModule = module {
factory { UserRepository(get()) } factory { UserRepository(get()) }
factory { TermsRepository(get()) } factory { TermsRepository(get()) }
factory { SeriesRepository(get()) }
factory { LiveRepository(get(), get(), get()) } factory { LiveRepository(get(), get(), get()) }
factory { EventRepository(get()) } factory { EventRepository(get()) }
factory { LiveRecommendRepository(get()) } factory { LiveRecommendRepository(get()) }
factory { AuthRepository(get()) } factory { AuthRepository(get()) }
factory { CanRepository(get()) } factory { CanRepository(get()) }
factory { CanPaymentTempRepository(get()) }
factory { LiveTagRepository(get()) } factory { LiveTagRepository(get()) }
factory { ReportRepository(get()) } factory { ReportRepository(get()) }
factory { ExplorerRepository(get()) } factory { ExplorerRepository(get()) }

View File

@ -121,7 +121,11 @@ class ExplorerAdapter(
) )
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position]) val item = items[position]
if (item.creators.isNotEmpty()) {
holder.bind(items[position])
}
} }
override fun getItemCount() = items.size override fun getItemCount() = items.size

View File

@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.explorer.profile package kr.co.vividnext.sodalive.explorer.profile
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.audio_content.series.GetSeriesListResponse
import kr.co.vividnext.sodalive.explorer.profile.creator_community.GetCommunityPostListResponse import kr.co.vividnext.sodalive.explorer.profile.creator_community.GetCommunityPostListResponse
data class GetCreatorProfileResponse( data class GetCreatorProfileResponse(
@ -20,6 +21,8 @@ data class GetCreatorProfileResponse(
val cheers: GetCheersResponse, val cheers: GetCheersResponse,
@SerializedName("activitySummary") @SerializedName("activitySummary")
val activitySummary: GetCreatorActivitySummary, val activitySummary: GetCreatorActivitySummary,
@SerializedName("seriesList")
val seriesList: List<GetSeriesListResponse.SeriesListItem>,
@SerializedName("isBlock") @SerializedName("isBlock")
val isBlock: Boolean val isBlock: Boolean
) )

View File

@ -26,6 +26,10 @@ import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.AudioContentActivity import kr.co.vividnext.sodalive.audio_content.AudioContentActivity
import kr.co.vividnext.sodalive.audio_content.AudioContentAdapter import kr.co.vividnext.sodalive.audio_content.AudioContentAdapter
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.series.GetSeriesListResponse
import kr.co.vividnext.sodalive.audio_content.series.SeriesListAdapter
import kr.co.vividnext.sodalive.audio_content.series.SeriesListAllActivity
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadActivity import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadActivity
import kr.co.vividnext.sodalive.base.BaseActivity import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.base.SodaDialog import kr.co.vividnext.sodalive.base.SodaDialog
@ -42,6 +46,7 @@ import kr.co.vividnext.sodalive.explorer.profile.donation.UserProfileDonationAda
import kr.co.vividnext.sodalive.explorer.profile.donation.UserProfileDonationAllViewActivity 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.fantalk.UserProfileFantalkAllViewActivity
import kr.co.vividnext.sodalive.explorer.profile.follow.UserFollowerListActivity import kr.co.vividnext.sodalive.explorer.profile.follow.UserFollowerListActivity
import kr.co.vividnext.sodalive.explorer.profile.series.UserProfileSeriesListAdapter
import kr.co.vividnext.sodalive.extensions.dpToPx import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.loadUrl import kr.co.vividnext.sodalive.extensions.loadUrl
import kr.co.vividnext.sodalive.extensions.moneyFormat import kr.co.vividnext.sodalive.extensions.moneyFormat
@ -70,6 +75,7 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
private lateinit var loadingDialog: LoadingDialog private lateinit var loadingDialog: LoadingDialog
private lateinit var liveAdapter: UserProfileLiveAdapter private lateinit var liveAdapter: UserProfileLiveAdapter
private lateinit var audioContentAdapter: AudioContentAdapter private lateinit var audioContentAdapter: AudioContentAdapter
private lateinit var seriesAdapter: UserProfileSeriesListAdapter
private lateinit var donationAdapter: UserProfileDonationAdapter private lateinit var donationAdapter: UserProfileDonationAdapter
private lateinit var cheersAdapter: UserProfileCheersAdapter private lateinit var cheersAdapter: UserProfileCheersAdapter
@ -117,6 +123,7 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
setupLiveView() setupLiveView()
setupDonationView() setupDonationView()
setupFanTalkView() setupFanTalkView()
setupSeriesListView()
setupAudioContentListView() setupAudioContentListView()
setupCreatorCommunityView() setupCreatorCommunityView()
} }
@ -415,6 +422,65 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
rvCheers.adapter = cheersAdapter rvCheers.adapter = cheersAdapter
} }
private fun setupSeriesListView() {
binding.layoutCreatorChannelSeries.tvAll.setOnClickListener {
startActivity(
Intent(applicationContext, SeriesListAllActivity::class.java).apply {
putExtra(Constants.EXTRA_USER_ID, userId)
}
)
}
val recyclerView = binding.layoutCreatorChannelSeries.rvSeries
recyclerView.layoutManager = LinearLayoutManager(
applicationContext,
LinearLayoutManager.HORIZONTAL,
false
)
seriesAdapter = UserProfileSeriesListAdapter(
onClickItem = {
startActivity(
Intent(applicationContext, SeriesDetailActivity::class.java).apply {
putExtra(Constants.EXTRA_SERIES_ID, it)
}
)
},
onClickCreator = {},
isVisibleCreator = false
)
recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.left = 0
outRect.right = 6.7f.dpToPx().toInt()
}
seriesAdapter.itemCount - 1 -> {
outRect.right = 0
outRect.left = 6.7f.dpToPx().toInt()
}
else -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 6.7f.dpToPx().toInt()
}
}
}
})
recyclerView.adapter = seriesAdapter
}
private fun setupAudioContentListView() { private fun setupAudioContentListView() {
binding.layoutUserProfileAudioContent.tvAll.setOnClickListener { binding.layoutUserProfileAudioContent.tvAll.setOnClickListener {
val intent = Intent(applicationContext, AudioContentActivity::class.java) val intent = Intent(applicationContext, AudioContentActivity::class.java)
@ -517,6 +583,7 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
viewModel.creatorProfileLiveData.observe(this) { viewModel.creatorProfileLiveData.observe(this) {
setCheers(it.cheers) setCheers(it.cheers)
setSeriesList(it.seriesList)
setCreatorProfile(it.creator) setCreatorProfile(it.creator)
setAudioContentList(it.contentList) setAudioContentList(it.contentList)
setLiveRoomList(it.liveRoomList) setLiveRoomList(it.liveRoomList)
@ -550,6 +617,19 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
} }
} }
@SuppressLint("NotifyDataSetChanged")
private fun setSeriesList(seriesList: List<GetSeriesListResponse.SeriesListItem>) {
if (seriesList.isNotEmpty()) {
binding.layoutCreatorChannelSeries.root.visibility = View.VISIBLE
} else {
binding.layoutCreatorChannelSeries.root.visibility = View.GONE
}
seriesAdapter.items.clear()
seriesAdapter.items.addAll(seriesList)
seriesAdapter.notifyDataSetChanged()
}
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
private fun setCreatorProfile(creator: CreatorResponse) { private fun setCreatorProfile(creator: CreatorResponse) {
val layoutUserProfile = binding.layoutUserProfile val layoutUserProfile = binding.layoutUserProfile

View File

@ -0,0 +1,98 @@
package kr.co.vividnext.sodalive.explorer.profile.series
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 coil.transform.RoundedCornersTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.series.GetSeriesListResponse
import kr.co.vividnext.sodalive.databinding.ItemSeriesListBigBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
class UserProfileSeriesListAdapter(
private val onClickItem: (Long) -> Unit,
private val onClickCreator: (Long) -> Unit,
private val isVisibleCreator: Boolean
) : RecyclerView.Adapter<UserProfileSeriesListAdapter.ViewHolder>() {
val items = mutableListOf<GetSeriesListResponse.SeriesListItem>()
inner class ViewHolder(
private val binding: ItemSeriesListBigBinding
) : RecyclerView.ViewHolder(binding.root) {
@SuppressLint("SetTextI18n")
fun bind(item: GetSeriesListResponse.SeriesListItem) {
binding.ivCover.load(item.coverImage) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(RoundedCornersTransformation(5f.dpToPx()))
}
binding.tvTitle.text = item.title
binding.tvSeriesContentCount.text = "${item.numberOfContent}"
binding.tvPublishedDaysOfWeek.text = item.publishedDaysOfWeek
binding.tvNew.visibility = if (item.isNew) {
View.VISIBLE
} else {
View.GONE
}
binding.tvPopular.visibility = if (item.isPopular) {
View.VISIBLE
} else {
View.GONE
}
if (item.isComplete) {
binding.tvNew.visibility = View.GONE
binding.tvComplete.visibility = View.VISIBLE
} else {
binding.tvComplete.visibility = View.GONE
}
if (isVisibleCreator) {
binding.llCreator.visibility = View.VISIBLE
binding.tvCreator.text = item.creator.nickname
binding.ivCreator.load(item.creator.profileImage) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}
binding.llCreator.setOnClickListener { onClickCreator(item.creator.creatorId) }
} else {
binding.llCreator.visibility = View.GONE
}
binding.root.setOnClickListener { onClickItem(item.seriesId) }
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
ItemSeriesListBigBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
}
override fun getItemCount() = items.count()
@SuppressLint("NotifyDataSetChanged")
fun addItems(items: List<GetSeriesListResponse.SeriesListItem>) {
this.items.addAll(items)
notifyDataSetChanged()
}
fun clear() {
this.items.clear()
}
}

View File

@ -21,6 +21,7 @@ import kr.co.vividnext.sodalive.live.room.donation.GetLiveRoomDonationStatusResp
import kr.co.vividnext.sodalive.live.room.donation.GetLiveRoomDonationTotalResponse import kr.co.vividnext.sodalive.live.room.donation.GetLiveRoomDonationTotalResponse
import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationMessage import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationMessage
import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationRequest import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationRequest
import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationResponse
import kr.co.vividnext.sodalive.live.room.info.GetRoomInfoResponse import kr.co.vividnext.sodalive.live.room.info.GetRoomInfoResponse
import kr.co.vividnext.sodalive.live.room.kick_out.LiveRoomKickOutRequest import kr.co.vividnext.sodalive.live.room.kick_out.LiveRoomKickOutRequest
import kr.co.vividnext.sodalive.live.room.profile.GetLiveRoomUserProfileResponse import kr.co.vividnext.sodalive.live.room.profile.GetLiveRoomUserProfileResponse
@ -156,11 +157,11 @@ interface LiveApi {
@Header("Authorization") authHeader: String @Header("Authorization") authHeader: String
): Single<ApiResponse<Any>> ): Single<ApiResponse<Any>>
@POST("/live/room/donation") @POST("/live/room/donation/v2")
fun donation( fun donation(
@Body request: LiveRoomDonationRequest, @Body request: LiveRoomDonationRequest,
@Header("Authorization") authHeader: String @Header("Authorization") authHeader: String
): Single<ApiResponse<String>> ): Single<ApiResponse<LiveRoomDonationResponse>>
@POST("/live/room/donation/refund/{id}") @POST("/live/room/donation/refund/{id}")
fun refundDonation( fun refundDonation(

View File

@ -129,8 +129,6 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
val intent = Intent(requireContext(), LiveRoomCreateActivity::class.java) val intent = Intent(requireContext(), LiveRoomCreateActivity::class.java)
activityResultLauncher.launch(intent) activityResultLauncher.launch(intent)
} }
binding.swipeRefreshLayout.setOnRefreshListener { refreshSummary() }
} }
private fun refreshSummary() { private fun refreshSummary() {
@ -140,8 +138,6 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
message = "라이브를 불러오고 있습니다." message = "라이브를 불러오고 있습니다."
viewModel.getSummary() viewModel.getSummary()
binding.swipeRefreshLayout.isRefreshing = false
} }
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
@ -184,7 +180,7 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
.setIndicatorVisibility(View.GONE) .setIndicatorVisibility(View.GONE)
.setIndicatorSliderColor( .setIndicatorSliderColor(
ContextCompat.getColor(requireContext(), R.color.color_909090), ContextCompat.getColor(requireContext(), R.color.color_909090),
ContextCompat.getColor(requireContext(), R.color.color_9970ff) ContextCompat.getColor(requireContext(), R.color.color_3bb9f1)
) )
.setIndicatorSliderWidth(4f.dpToPx().toInt(), 10f.dpToPx().toInt()) .setIndicatorSliderWidth(4f.dpToPx().toInt(), 10f.dpToPx().toInt())
.setIndicatorHeight(4f.dpToPx().toInt()) .setIndicatorHeight(4f.dpToPx().toInt())
@ -287,6 +283,11 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
startActivity(Intent(requireContext(), LiveNowAllActivity::class.java)) startActivity(Intent(requireContext(), LiveNowAllActivity::class.java))
} }
binding
.layoutLiveNow
.llRefresh
.setOnClickListener { refreshSummary() }
val recyclerView = binding val recyclerView = binding
.layoutLiveNow .layoutLiveNow
.rvSudaNow .rvSudaNow
@ -486,7 +487,7 @@ class LiveFragment : BaseFragment<FragmentLiveBinding>(FragmentLiveBinding::infl
.setIndicatorVisibility(View.GONE) .setIndicatorVisibility(View.GONE)
.setIndicatorSliderColor( .setIndicatorSliderColor(
ContextCompat.getColor(requireContext(), R.color.color_909090), ContextCompat.getColor(requireContext(), R.color.color_909090),
ContextCompat.getColor(requireContext(), R.color.color_9970ff) ContextCompat.getColor(requireContext(), R.color.color_3bb9f1)
) )
.setIndicatorSliderWidth(4f.dpToPx().toInt(), 10f.dpToPx().toInt()) .setIndicatorSliderWidth(4f.dpToPx().toInt(), 10f.dpToPx().toInt())
.setIndicatorHeight(4f.dpToPx().toInt()) .setIndicatorHeight(4f.dpToPx().toInt())

View File

@ -14,6 +14,7 @@ import kr.co.vividnext.sodalive.live.room.create.CreateLiveRoomResponse
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailResponse import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailResponse
import kr.co.vividnext.sodalive.live.room.donation.DeleteLiveRoomDonationMessage import kr.co.vividnext.sodalive.live.room.donation.DeleteLiveRoomDonationMessage
import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationRequest import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationRequest
import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationResponse
import kr.co.vividnext.sodalive.live.room.kick_out.LiveRoomKickOutRequest import kr.co.vividnext.sodalive.live.room.kick_out.LiveRoomKickOutRequest
import kr.co.vividnext.sodalive.live.room.menu.MenuApi import kr.co.vividnext.sodalive.live.room.menu.MenuApi
import kr.co.vividnext.sodalive.user.CreatorFollowRequestRequest import kr.co.vividnext.sodalive.user.CreatorFollowRequestRequest
@ -163,7 +164,7 @@ class LiveRepository(
can: Int, can: Int,
message: String, message: String,
token: String token: String
): Single<ApiResponse<String>> { ): Single<ApiResponse<LiveRoomDonationResponse>> {
return api.donation( return api.donation(
request = LiveRoomDonationRequest( request = LiveRoomDonationRequest(
roomId = roomId, roomId = roomId,

View File

@ -68,6 +68,7 @@ import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationDialog
import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationMessageDialog import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationMessageDialog
import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationMessageViewModel import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationMessageViewModel
import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationRankingDialog import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationRankingDialog
import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationResponse
import kr.co.vividnext.sodalive.live.room.info.GetRoomInfoResponse import kr.co.vividnext.sodalive.live.room.info.GetRoomInfoResponse
import kr.co.vividnext.sodalive.live.room.profile.LiveRoomProfileDialog import kr.co.vividnext.sodalive.live.room.profile.LiveRoomProfileDialog
import kr.co.vividnext.sodalive.live.room.profile.LiveRoomProfileListAdapter import kr.co.vividnext.sodalive.live.room.profile.LiveRoomProfileListAdapter
@ -124,6 +125,16 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
} }
} }
private val signatureList = mutableListOf<LiveRoomDonationResponse>()
private var signature: LiveRoomDonationResponse? = null
set(value) {
field = value
if (field != null) {
showSignatureImage()
}
}
private var isShowSignatureImage = false private var isShowSignatureImage = false
private val countDownTimer = object : CountDownTimer(remainingNoChattingTime * 1000, 1000) { private val countDownTimer = object : CountDownTimer(remainingNoChattingTime * 1000, 1000) {
@ -468,6 +479,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
binding.tvMenuPan.setOnClickListener { viewModel.toggleShowMenuPan() } binding.tvMenuPan.setOnClickListener { viewModel.toggleShowMenuPan() }
binding.tvBgSwitch.setOnClickListener { viewModel.toggleBackgroundImage() } binding.tvBgSwitch.setOnClickListener { viewModel.toggleBackgroundImage() }
binding.tvSignatureSwitch.setOnClickListener { viewModel.toggleSignatureImage() }
binding.llDonation.setOnClickListener { binding.llDonation.setOnClickListener {
LiveRoomDonationRankingDialog( LiveRoomDonationRankingDialog(
activity = this, activity = this,
@ -626,6 +638,31 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
} }
} }
viewModel.isSignatureOn.observe(this) {
if (it) {
binding.tvSignatureSwitch.text = "시그 ON"
binding.tvSignatureSwitch.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_3bb9f1
)
)
binding.tvSignatureSwitch
.setBackgroundResource(R.drawable.bg_round_corner_5_3_transparent_3bb9f1)
} else {
binding.tvSignatureSwitch.text = "시그 OFF"
binding.tvSignatureSwitch.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_eeeeee
)
)
binding.tvSignatureSwitch
.setBackgroundResource(R.drawable.bg_round_corner_5_3_transparent_bbbbbb)
binding.ivSignature.visibility = View.GONE
}
}
viewModel.isLoading.observe(this) { viewModel.isLoading.observe(this) {
if (it) { if (it) {
loadingDialog.show(screenWidth) loadingDialog.show(screenWidth)
@ -691,7 +728,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
copyMessage = { copyMessage = {
val clipboard = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager val clipboard = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setPrimaryClip(ClipData.newPlainText(it, it)) clipboard.setPrimaryClip(ClipData.newPlainText(it, it))
showToast("후원 메시지가 복사되었습니다.") showToast("후원 히스토리가 복사되었습니다.")
} }
).show() ).show()
} }
@ -759,21 +796,6 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
binding.ivEdit.visibility = View.GONE binding.ivEdit.visibility = View.GONE
} }
binding.ivShare.setOnClickListener {
viewModel.shareRoomLink(
response.roomId,
response.isPrivateRoom,
response.password
) {
val intent = Intent(Intent.ACTION_SEND)
intent.type = "text/plain"
intent.putExtra(Intent.EXTRA_TEXT, it)
val shareIntent = Intent.createChooser(intent, "라이브 공유")
startActivity(shareIntent)
}
}
if (response.creatorId == SharedPreferenceManager.userId) { if (response.creatorId == SharedPreferenceManager.userId) {
binding.llViewUsers.visibility = View.VISIBLE binding.llViewUsers.visibility = View.VISIBLE
binding.llViewUsers.setOnClickListener { roomProfileDialog.show() } binding.llViewUsers.setOnClickListener { roomProfileDialog.show() }
@ -928,8 +950,10 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
viewModel.showRoulette { viewModel.showRoulette {
RoulettePreviewDialog( RoulettePreviewDialog(
activity = this, activity = this,
preview = it, previewList = it,
onClickSpin = { spinRoulette() }, onClickSpin = { rouletteId ->
spinRoulette(rouletteId = rouletteId)
},
layoutInflater = layoutInflater layoutInflater = layoutInflater
).show() ).show()
} }
@ -1270,13 +1294,14 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
private fun donation(can: Int, message: String) { private fun donation(can: Int, message: String) {
val rawMessage = "${can}캔을 후원하셨습니다.\uD83D\uDCB0\uD83E\uDE99" val rawMessage = "${can}캔을 후원하셨습니다.\uD83D\uDCB0\uD83E\uDE99"
viewModel.donation(roomId, can, message) { signatureImage -> viewModel.donation(roomId, can, message) { signature ->
val donationRawMessage = Gson().toJson( val donationRawMessage = Gson().toJson(
LiveRoomChatRawMessage( LiveRoomChatRawMessage(
type = LiveRoomChatRawMessageType.DONATION, type = LiveRoomChatRawMessageType.DONATION,
message = rawMessage, message = rawMessage,
can = can, can = can,
signatureImageUrl = signatureImage, signature = signature,
signatureImageUrl = signature?.imageUrl,
donationMessage = message donationMessage = message
) )
) )
@ -1300,7 +1325,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
) )
invalidateChat() invalidateChat()
viewModel.addDonationCan(can) viewModel.addDonationCan(can)
addSignatureImage(signatureImage) addSignature(signature)
} }
}, },
onFailure = { onFailure = {
@ -1310,12 +1335,12 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
} }
} }
private fun spinRoulette() { private fun spinRoulette(rouletteId: Long) {
viewModel.spinRoulette(roomId = roomId) { can, items, randomlySelectedItem -> viewModel.spinRoulette(roomId = roomId, rouletteId = rouletteId) { can, items, randomItem ->
val rouletteRawMessage = Gson().toJson( val rouletteRawMessage = Gson().toJson(
LiveRoomChatRawMessage( LiveRoomChatRawMessage(
type = LiveRoomChatRawMessageType.ROULETTE_DONATION, type = LiveRoomChatRawMessageType.ROULETTE_DONATION,
message = randomlySelectedItem, message = randomItem,
can = can, can = can,
donationMessage = "", donationMessage = "",
) )
@ -1324,7 +1349,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
RouletteSpinDialog( RouletteSpinDialog(
activity = this@LiveRoomActivity, activity = this@LiveRoomActivity,
items = items, items = items,
selectedItem = randomlySelectedItem, selectedItem = randomItem,
layoutInflater = layoutInflater layoutInflater = layoutInflater
) { ) {
agora.sendRawMessageToGroup( agora.sendRawMessageToGroup(
@ -1335,7 +1360,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
LiveRoomRouletteDonationChat( LiveRoomRouletteDonationChat(
profileUrl = SharedPreferenceManager.profileImage, profileUrl = SharedPreferenceManager.profileImage,
nickname = SharedPreferenceManager.nickname, nickname = SharedPreferenceManager.nickname,
rouletteResult = randomlySelectedItem rouletteResult = randomItem
) )
) )
invalidateChat() invalidateChat()
@ -1407,9 +1432,12 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
) )
invalidateChat() invalidateChat()
viewModel.addDonationCan(rawMessage.can) viewModel.addDonationCan(rawMessage.can)
addSignatureImage(
imageUrl = rawMessage.signatureImageUrl ?: "" if (rawMessage.signature != null) {
) addSignature(rawMessage.signature)
} else if (rawMessage.signatureImageUrl != null) {
addSignatureImage(rawMessage.signatureImageUrl)
}
} }
} }
@ -1762,14 +1790,47 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
} }
} }
private fun showSignatureImage() { private fun addSignature(signature: LiveRoomDonationResponse?) {
if (signatureImageUrl.isNotBlank()) { if (signature != null) {
Glide if (!isShowSignatureImage) {
.with(this) this.signature = signature
.load(signatureImageUrl) isShowSignatureImage = true
.into(binding.ivSignature) } else {
signatureList.add(signature)
}
}
}
binding.ivSignature.visibility = View.VISIBLE private fun showSignatureImage() {
if (signature != null) {
if (viewModel.isSignatureOn.value!!) {
Glide
.with(this)
.load(signature!!.imageUrl)
.into(binding.ivSignature)
binding.ivSignature.visibility = View.VISIBLE
}
handler.postDelayed({
if (signatureList.isNotEmpty()) {
signature = signatureList.removeAt(0)
} else {
signature = null
isShowSignatureImage = false
binding.ivSignature.setImageDrawable(null)
binding.ivSignature.visibility = View.GONE
}
}, signature!!.time * 1000L)
} else if (signatureImageUrl.isNotBlank()) {
if (viewModel.isSignatureOn.value!!) {
Glide
.with(this)
.load(signatureImageUrl)
.into(binding.ivSignature)
binding.ivSignature.visibility = View.VISIBLE
}
handler.postDelayed({ handler.postDelayed({
if (signatureImageUrlList.isNotEmpty()) { if (signatureImageUrlList.isNotEmpty()) {
signatureImageUrl = signatureImageUrlList.removeAt(0) signatureImageUrl = signatureImageUrlList.removeAt(0)

View File

@ -3,12 +3,6 @@ package kr.co.vividnext.sodalive.live.room
import android.net.Uri import android.net.Uri
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData 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.google.gson.Gson import com.google.gson.Gson
import com.orhanobut.logger.Logger import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
@ -17,6 +11,7 @@ import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.live.LiveRepository import kr.co.vividnext.sodalive.live.LiveRepository
import kr.co.vividnext.sodalive.live.room.donation.GetLiveRoomDonationStatusResponse import kr.co.vividnext.sodalive.live.room.donation.GetLiveRoomDonationStatusResponse
import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationResponse
import kr.co.vividnext.sodalive.live.room.info.GetRoomInfoResponse import kr.co.vividnext.sodalive.live.room.info.GetRoomInfoResponse
import kr.co.vividnext.sodalive.live.room.menu.GetMenuPresetResponse import kr.co.vividnext.sodalive.live.room.menu.GetMenuPresetResponse
import kr.co.vividnext.sodalive.live.room.profile.GetLiveRoomUserProfileResponse import kr.co.vividnext.sodalive.live.room.profile.GetLiveRoomUserProfileResponse
@ -35,7 +30,7 @@ import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File import java.io.File
import kotlin.math.floor import java.util.Locale
class LiveRoomViewModel( class LiveRoomViewModel(
private val repository: LiveRepository, private val repository: LiveRepository,
@ -83,6 +78,10 @@ class LiveRoomViewModel(
val isBgOn: LiveData<Boolean> val isBgOn: LiveData<Boolean>
get() = _isBgOn get() = _isBgOn
private var _isSignatureOn = MutableLiveData(true)
val isSignatureOn: LiveData<Boolean>
get() = _isSignatureOn
lateinit var getRealPathFromURI: (Uri) -> String? lateinit var getRealPathFromURI: (Uri) -> String?
fun getUserNickname(memberId: Int): String { fun getUserNickname(memberId: Int): String {
@ -248,43 +247,6 @@ class LiveRoomViewModel(
) )
} }
fun shareRoomLink(
roomId: Long,
isPrivateRoom: Boolean,
password: String?,
onSuccess: (String) -> Unit
) {
_isLoading.value = true
Firebase.dynamicLinks.shortLinkAsync(ShortDynamicLink.Suffix.SHORT) {
link = Uri.parse("https://sodalive.net/?room_id=$roomId")
domainUriPrefix = "https://sodalive.page.link"
androidParameters { }
iosParameters("kr.co.vividnext.sodalive") {
appStoreId = "6461721697"
}
}.addOnSuccessListener {
val uri = it.shortLink
if (uri != null) {
val message = if (isPrivateRoom) {
"${SharedPreferenceManager.nickname}님이 귀하를 " +
"소다라이브의 비공개라이브에 초대하였습니다.\n" +
"※ 라이브 참여: $uri\n" +
"(입장 비밀번호 : $password)"
} else {
"${SharedPreferenceManager.nickname}님이 귀하를 " +
"소다라이브의 공개라이브에 초대하였습니다.\n" +
"※ 라이브 참여: $uri"
}
onSuccess(message)
}
}.addOnFailureListener {
_toastLiveData.postValue("공유링크를 생성하지 못했습니다.\n다시 시도해 주세요.")
}.addOnCompleteListener {
_isLoading.value = false
}
}
fun creatorFollow(creatorId: Long, roomId: Long, isGetUserProfile: Boolean = false) { fun creatorFollow(creatorId: Long, roomId: Long, isGetUserProfile: Boolean = false) {
_isLoading.value = true _isLoading.value = true
compositeDisposable.add( compositeDisposable.add(
@ -378,6 +340,10 @@ class LiveRoomViewModel(
_isBgOn.value = !isBgOn.value!! _isBgOn.value = !isBgOn.value!!
} }
fun toggleSignatureImage() {
_isSignatureOn.value = !isSignatureOn.value!!
}
fun editLiveRoomInfo( fun editLiveRoomInfo(
roomId: Long, roomId: Long,
newTitle: String, newTitle: String,
@ -508,7 +474,12 @@ class LiveRoomViewModel(
) )
} }
fun donation(roomId: Long, can: Int, message: String, onSuccess: (String) -> Unit) { fun donation(
roomId: Long,
can: Int,
message: String,
onSuccess: (LiveRoomDonationResponse?) -> Unit
) {
_isLoading.postValue(true) _isLoading.postValue(true)
compositeDisposable.add( compositeDisposable.add(
repository.donation(roomId, can, message, "Bearer ${SharedPreferenceManager.token}") repository.donation(roomId, can, message, "Bearer ${SharedPreferenceManager.token}")
@ -519,7 +490,7 @@ class LiveRoomViewModel(
_isLoading.value = false _isLoading.value = false
if (it.success) { if (it.success) {
SharedPreferenceManager.can -= can SharedPreferenceManager.can -= can
onSuccess(it.data ?: "") onSuccess(it.data)
} else { } else {
if (it.message != null) { if (it.message != null) {
_toastLiveData.postValue(it.message) _toastLiveData.postValue(it.message)
@ -808,7 +779,7 @@ class LiveRoomViewModel(
) )
} }
fun showRoulette(complete: (RoulettePreview) -> Unit) { fun showRoulette(complete: (List<RoulettePreview>) -> Unit) {
if (!_isLoading.value!!) { if (!_isLoading.value!!) {
_isLoading.value = true _isLoading.value = true
compositeDisposable.add( compositeDisposable.add(
@ -825,15 +796,19 @@ class LiveRoomViewModel(
val data = it.data val data = it.data
if ( if (
it.success && it.success &&
data != null && !data.isNullOrEmpty()
data.isActive &&
data.items.isNotEmpty()
) { ) {
complete( complete(
RoulettePreview( data
data.can, .filter { roulette -> roulette.isActive }
items = calculatePercentages(data.items) .filter { roulette -> roulette.items.isNotEmpty() }
) .map { roulette ->
RoulettePreview(
id = roulette.id,
can = roulette.can,
items = calculatePercentages(roulette.items)
)
}
) )
} else { } else {
val message = it.message ?: "룰렛을 사용할 수 없습니다. 다시 시도해 주세요." val message = it.message ?: "룰렛을 사용할 수 없습니다. 다시 시도해 주세요."
@ -850,12 +825,16 @@ class LiveRoomViewModel(
} }
} }
fun spinRoulette(roomId: Long, complete: (Int, List<RouletteItem>, String) -> Unit) { fun spinRoulette(
roomId: Long,
rouletteId: Long,
complete: (Int, List<RouletteItem>, String) -> Unit
) {
if (!_isLoading.value!!) { if (!_isLoading.value!!) {
_isLoading.value = true _isLoading.value = true
compositeDisposable.add( compositeDisposable.add(
rouletteRepository.spinRoulette( rouletteRepository.spinRoulette(
request = SpinRouletteRequest(roomId = roomId), request = SpinRouletteRequest(roomId = roomId, rouletteId = rouletteId),
token = "Bearer ${SharedPreferenceManager.token}" token = "Bearer ${SharedPreferenceManager.token}"
) )
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
@ -868,11 +847,10 @@ class LiveRoomViewModel(
if ( if (
it.success && it.success &&
data != null && data != null &&
data.isActive &&
data.items.isNotEmpty() data.items.isNotEmpty()
) { ) {
SharedPreferenceManager.can -= data.can SharedPreferenceManager.can -= data.can
randomSelectRouletteItem(data.can, data.items, complete) complete(data.can, data.items, data.result)
} else { } else {
val message = it.message ?: "룰렛을 사용할 수 없습니다. 다시 시도해 주세요." val message = it.message ?: "룰렛을 사용할 수 없습니다. 다시 시도해 주세요."
_toastLiveData.postValue(message) _toastLiveData.postValue(message)
@ -961,31 +939,11 @@ class LiveRoomViewModel(
) )
} }
private fun randomSelectRouletteItem(
can: Int,
items: List<RouletteItem>,
complete: (Int, List<RouletteItem>, String) -> Unit
) {
_isLoading.value = true
val rouletteItems = mutableListOf<String>()
items.asSequence().forEach { item ->
repeat(item.weight * 10) {
rouletteItems.add(item.title)
}
}
_isLoading.value = false
complete(can, items, rouletteItems.random())
}
private fun calculatePercentages(options: List<RouletteItem>): List<RoulettePreviewItem> { private fun calculatePercentages(options: List<RouletteItem>): List<RoulettePreviewItem> {
val totalWeight = options.sumOf { it.weight } val updatedOptions = options.map { option ->
val updatedOptions = options.asSequence().map { option ->
val percent = floor(option.weight.toDouble() / totalWeight * 10000) / 100
RoulettePreviewItem( RoulettePreviewItem(
title = option.title, title = option.title,
percent = "${String.format("%.2f", percent)}%" percent = "${String.format(Locale.KOREAN, "%.2f", option.percentage)}%"
) )
}.toList() }.toList()

View File

@ -1,11 +1,13 @@
package kr.co.vividnext.sodalive.live.room.chat package kr.co.vividnext.sodalive.live.room.chat
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationResponse
data class LiveRoomChatRawMessage( data class LiveRoomChatRawMessage(
@SerializedName("type") val type: LiveRoomChatRawMessageType, @SerializedName("type") val type: LiveRoomChatRawMessageType,
@SerializedName("message") val message: String, @SerializedName("message") val message: String,
@SerializedName("can") val can: Int, @SerializedName("can") val can: Int,
@SerializedName("signature") val signature: LiveRoomDonationResponse? = null,
@SerializedName("signatureImageUrl") val signatureImageUrl: String? = null, @SerializedName("signatureImageUrl") val signatureImageUrl: String? = null,
@SerializedName("donationMessage") val donationMessage: String?, @SerializedName("donationMessage") val donationMessage: String?,
@SerializedName("isActiveRoulette") val isActiveRoulette: Boolean? = null @SerializedName("isActiveRoulette") val isActiveRoulette: Boolean? = null

View File

@ -17,5 +17,6 @@ data class CreateLiveRoomRequest(
@SerializedName("password") val password: String? = null, @SerializedName("password") val password: String? = null,
@SerializedName("menuPanId") val menuPanId: Long = 0, @SerializedName("menuPanId") val menuPanId: Long = 0,
@SerializedName("menuPan") val menuPan: String = "", @SerializedName("menuPan") val menuPan: String = "",
@SerializedName("isActiveMenuPan") val isActiveMenuPan: Boolean = false @SerializedName("isActiveMenuPan") val isActiveMenuPan: Boolean = false,
@SerializedName("isAvailableJoinCreator") val isAvailableJoinCreator: Boolean = true
) )

View File

@ -322,6 +322,14 @@ class LiveRoomCreateActivity : BaseActivity<ActivityLiveRoomCreateBinding>(
binding.llSelectMenu3.setOnClickListener { binding.llSelectMenu3.setOnClickListener {
viewModel.selectMenuPreset(LiveRoomCreateViewModel.SelectedMenu.MENU_3) viewModel.selectMenuPreset(LiveRoomCreateViewModel.SelectedMenu.MENU_3)
} }
binding.llAvailableJoinCreatorY.setOnClickListener {
viewModel.setAvailableJoinCreator(true)
}
binding.llAvailableJoinCreatorN.setOnClickListener {
viewModel.setAvailableJoinCreator(false)
}
} }
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
@ -647,6 +655,46 @@ class LiveRoomCreateActivity : BaseActivity<ActivityLiveRoomCreateBinding>(
viewModel.menuLiveData.observe(this) { viewModel.menuLiveData.observe(this) {
binding.etMenu.setText(it) binding.etMenu.setText(it)
} }
viewModel.isAvailableJoinCreatorLiveData.observe(this) {
if (it) {
binding.ivAvailableJoinCreatorN.visibility = View.GONE
binding.llAvailableJoinCreatorN.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b)
binding.tvAvailableJoinCreatorN.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_3bb9f1
)
)
binding.ivAvailableJoinCreatorY.visibility = View.VISIBLE
binding.llAvailableJoinCreatorY.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.tvAvailableJoinCreatorY.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_eeeeee
)
)
} else {
binding.ivAvailableJoinCreatorY.visibility = View.GONE
binding.llAvailableJoinCreatorY.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b)
binding.tvAvailableJoinCreatorY.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_3bb9f1
)
)
binding.ivAvailableJoinCreatorN.visibility = View.VISIBLE
binding.llAvailableJoinCreatorN.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
binding.tvAvailableJoinCreatorN.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_eeeeee
)
)
}
}
} }
} }

View File

@ -71,6 +71,10 @@ class LiveRoomCreateViewModel(
val isActivateMenuLiveData: LiveData<Boolean> val isActivateMenuLiveData: LiveData<Boolean>
get() = _isActivateMenuLiveData get() = _isActivateMenuLiveData
private val _isAvailableJoinCreatorLiveData = MutableLiveData(true)
val isAvailableJoinCreatorLiveData: LiveData<Boolean>
get() = _isAvailableJoinCreatorLiveData
private val _menuLiveData = MutableLiveData("") private val _menuLiveData = MutableLiveData("")
val menuLiveData: LiveData<String> val menuLiveData: LiveData<String>
get() = _menuLiveData get() = _menuLiveData
@ -147,7 +151,8 @@ class LiveRoomCreateViewModel(
} else { } else {
"" ""
}, },
isActiveMenuPan = _isActivateMenuLiveData.value!! isActiveMenuPan = _isActivateMenuLiveData.value!!,
isAvailableJoinCreator = _isAvailableJoinCreatorLiveData.value!!
) )
val requestJson = Gson().toJson(request) val requestJson = Gson().toJson(request)
@ -255,6 +260,10 @@ class LiveRoomCreateViewModel(
_isAdultLiveData.value = isAdult _isAdultLiveData.value = isAdult
} }
fun setAvailableJoinCreator(isAvailableJoinCreator: Boolean) {
_isAvailableJoinCreatorLiveData.value = isAvailableJoinCreator
}
fun getRecentInfo(onSuccess: (GetRecentRoomInfoResponse) -> Unit) { fun getRecentInfo(onSuccess: (GetRecentRoomInfoResponse) -> Unit) {
_isLoading.value = true _isLoading.value = true
compositeDisposable.add( compositeDisposable.add(

View File

@ -111,6 +111,14 @@ class LiveRoomDonationDialog(
intent.putExtra(Constants.EXTRA_GO_TO_PREV_PAGE, true) intent.putExtra(Constants.EXTRA_GO_TO_PREV_PAGE, true)
activity.startActivity(intent) activity.startActivity(intent)
} }
dialogView.tvCharge.setOnClickListener {
bottomSheetDialog.dismiss()
val intent = Intent(activity, CanChargeActivity::class.java)
intent.putExtra(Constants.EXTRA_GO_TO_PREV_PAGE, true)
activity.startActivity(intent)
}
} }
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")

View File

@ -2,9 +2,11 @@ package kr.co.vividnext.sodalive.live.room.donation
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemLiveRoomDonationMessageBinding import kr.co.vividnext.sodalive.databinding.ItemLiveRoomDonationMessageBinding
class LiveRoomDonationMessageAdapter( class LiveRoomDonationMessageAdapter(
@ -20,8 +22,16 @@ class LiveRoomDonationMessageAdapter(
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
fun bind(item: LiveRoomDonationMessage) { fun bind(item: LiveRoomDonationMessage) {
binding.tvNickname.text = "${item.nickname}님이" if (item.canMessage.isNotBlank()) {
binding.tvCanMessage.text = item.canMessage binding.tvNickname.text = "${item.nickname}님이"
binding.tvCanMessage.text = item.canMessage
binding.tvCanMessage.visibility = View.VISIBLE
binding.root.setBackgroundResource(R.drawable.bg_round_corner_5_3_333333)
} else {
binding.tvNickname.text = "${item.nickname}님의 룰렛 결과?"
binding.tvCanMessage.visibility = View.GONE
binding.root.setBackgroundResource(R.drawable.bg_round_corner_5_3_ccc25264)
}
binding.tvDonationMessage.text = "\"${item.donationMessage}\"" binding.tvDonationMessage.text = "\"${item.donationMessage}\""
binding.ivDelete.setOnClickListener { onClickDeleteMessage(item.uuid) } binding.ivDelete.setOnClickListener { onClickDeleteMessage(item.uuid) }

View File

@ -0,0 +1,8 @@
package kr.co.vividnext.sodalive.live.room.donation
import com.google.gson.annotations.SerializedName
data class LiveRoomDonationResponse(
@SerializedName("imageUrl") val imageUrl: String,
@SerializedName("time") val time: Int
)

View File

@ -26,7 +26,7 @@ class LiveTagAdapter(
fun bind(item: GetLiveTagResponse) { fun bind(item: GetLiveTagResponse) {
if (selectedTags.contains(item.tag)) { if (selectedTags.contains(item.tag)) {
binding.ivTagChecked.visibility = View.VISIBLE binding.ivTagChecked.visibility = View.VISIBLE
binding.tvTag.setTextColor(ContextCompat.getColor(context, R.color.color_9970ff)) binding.tvTag.setTextColor(ContextCompat.getColor(context, R.color.color_3bb9f1))
isChecked = true isChecked = true
} else { } else {
binding.ivTagChecked.visibility = View.GONE binding.ivTagChecked.visibility = View.GONE
@ -50,7 +50,7 @@ class LiveTagAdapter(
binding.tvTag.setTextColor( binding.tvTag.setTextColor(
ContextCompat.getColor( ContextCompat.getColor(
context, context,
R.color.color_9970ff R.color.color_3bb9f1
) )
) )
} else { } else {

View File

@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.live.roulette
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
data class GetRouletteResponse( data class GetRouletteResponse(
@SerializedName("id") val id: Long,
@SerializedName("can") val can: Int, @SerializedName("can") val can: Int,
@SerializedName("isActive") val isActive: Boolean, @SerializedName("isActive") val isActive: Boolean,
@SerializedName("items") val items: List<RouletteItem> @SerializedName("items") val items: List<RouletteItem>
@ -10,5 +11,5 @@ data class GetRouletteResponse(
data class RouletteItem( data class RouletteItem(
@SerializedName("title") val title: String, @SerializedName("title") val title: String,
@SerializedName("weight") val weight: Int @SerializedName("percentage") val percentage: Float
) )

View File

@ -1,11 +1,14 @@
package kr.co.vividnext.sodalive.live.roulette package kr.co.vividnext.sodalive.live.roulette
import com.google.gson.annotations.SerializedName
data class RoulettePreview( data class RoulettePreview(
val can: Int, @SerializedName("id") val id: Long,
val items: List<RoulettePreviewItem> @SerializedName("can") val can: Int,
@SerializedName("items") val items: List<RoulettePreviewItem>
) )
data class RoulettePreviewItem( data class RoulettePreviewItem(
val title: String, @SerializedName("title") val title: String,
val percent: String @SerializedName("percent") val percent: String
) )

View File

@ -8,27 +8,36 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.Window import android.view.Window
import android.view.WindowManager import android.view.WindowManager
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.MutableLiveData
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.Constants import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.DialogRoulettePreviewBinding import kr.co.vividnext.sodalive.databinding.DialogRoulettePreviewBinding
import kr.co.vividnext.sodalive.extensions.dpToPx import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.extensions.moneyFormat import kr.co.vividnext.sodalive.extensions.moneyFormat
import kr.co.vividnext.sodalive.live.roulette.config.RouletteSettingsViewModel.SelectedRoulette
import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeActivity import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeActivity
class RoulettePreviewDialog( class RoulettePreviewDialog(
private val activity: FragmentActivity, private val activity: FragmentActivity,
private val preview: RoulettePreview, private val previewList: List<RoulettePreview>,
private val title: String = "", private val title: String = "",
private val onClickSpin: (() -> Unit)? = null, private val onClickSpin: ((Long) -> Unit)? = null,
layoutInflater: LayoutInflater layoutInflater: LayoutInflater
) { ) {
private val alertDialog: AlertDialog private val alertDialog: AlertDialog
private val dialogView = DialogRoulettePreviewBinding.inflate(layoutInflater) private val dialogView = DialogRoulettePreviewBinding.inflate(layoutInflater)
private val selectedRouletteLiveData = MutableLiveData(
SelectedRoulette.ROULETTE_1
)
init { init {
val dialogBuilder = AlertDialog.Builder(activity) val dialogBuilder = AlertDialog.Builder(activity)
dialogBuilder.setView(dialogView.root) dialogBuilder.setView(dialogView.root)
@ -54,18 +63,189 @@ class RoulettePreviewDialog(
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
private fun setupView() { private fun setupView() {
dialogView.tvCancel.setOnClickListener { alertDialog.dismiss() } if (previewList.isEmpty()) {
alertDialog.dismiss()
} else {
initSelectRouletteButton()
setRouletteData(previewList[0])
}
dialogView.tvSpinRoulette.text = "${preview.can}캔으로 룰렛 돌리기" dialogView.tvCancel.setOnClickListener { alertDialog.dismiss() }
}
private fun initSelectRouletteButton() {
if (previewList.size < 2) {
dialogView.llSelectRoulette.visibility = View.GONE
} else {
dialogView.llSelectRoulette.visibility = View.VISIBLE
if (previewList.size > 2) {
dialogView.llSelectRoulette3.visibility = View.VISIBLE
} else {
dialogView.llSelectRoulette3.visibility = View.GONE
}
}
dialogView.llSelectRoulette1.setOnClickListener {
if (selectedRouletteLiveData.value != SelectedRoulette.ROULETTE_1) {
selectedRouletteLiveData.value = SelectedRoulette.ROULETTE_1
}
}
dialogView.llSelectRoulette2.setOnClickListener {
if (selectedRouletteLiveData.value != SelectedRoulette.ROULETTE_2) {
selectedRouletteLiveData.value = SelectedRoulette.ROULETTE_2
}
}
dialogView.llSelectRoulette3.setOnClickListener {
if (selectedRouletteLiveData.value != SelectedRoulette.ROULETTE_3) {
selectedRouletteLiveData.value = SelectedRoulette.ROULETTE_3
}
}
selectedRouletteLiveData.observe(activity) {
deselectAllRoulette()
when (it) {
SelectedRoulette.ROULETTE_2 -> {
selectRouletteButton(
dialogView.ivSelectRoulette2,
dialogView.llSelectRoulette2,
dialogView.tvSelectRoulette2
)
setRouletteData(previewList[1])
dialogView.tvCancel.setTextColor(
ContextCompat.getColor(activity, R.color.color_ffcb14)
)
dialogView.tvCancel.setBackgroundResource(
R.drawable.bg_round_corner_10_transparent_ffcb14
)
}
SelectedRoulette.ROULETTE_3 -> {
selectRouletteButton(
dialogView.ivSelectRoulette3,
dialogView.llSelectRoulette3,
dialogView.tvSelectRoulette3
)
setRouletteData(previewList[2])
dialogView.tvCancel.setTextColor(
ContextCompat.getColor(activity, R.color.color_ff14d9)
)
dialogView.tvCancel.setBackgroundResource(
R.drawable.bg_round_corner_10_transparent_ff14d9
)
}
else -> {
selectRouletteButton(
dialogView.ivSelectRoulette1,
dialogView.llSelectRoulette1,
dialogView.tvSelectRoulette1
)
setRouletteData(previewList[0])
dialogView.tvCancel.setTextColor(
ContextCompat.getColor(activity, R.color.color_3bb9f1)
)
dialogView.tvCancel.setBackgroundResource(
R.drawable.bg_round_corner_10_transparent_3bb9f1
)
}
}
}
}
private fun deselectAllRoulette() {
dialogView.ivSelectRoulette1.visibility = View.GONE
dialogView.ivSelectRoulette2.visibility = View.GONE
dialogView.ivSelectRoulette3.visibility = View.GONE
dialogView.llSelectRoulette1.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b)
dialogView.tvSelectRoulette1.setTextColor(
ContextCompat.getColor(
activity,
R.color.color_3bb9f1
)
)
dialogView.llSelectRoulette2.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b)
dialogView.tvSelectRoulette2.setTextColor(
ContextCompat.getColor(
activity,
R.color.color_ffcb14
)
)
dialogView.llSelectRoulette3.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b)
dialogView.tvSelectRoulette3.setTextColor(
ContextCompat.getColor(
activity,
R.color.color_ff14d9
)
)
}
private fun selectRouletteButton(
ivSelectRoulette: ImageView,
llSelectRoulette: LinearLayout,
tvSelectRoulette: TextView
) {
ivSelectRoulette.visibility = View.VISIBLE
llSelectRoulette.setBackgroundResource(
when (selectedRouletteLiveData.value) {
SelectedRoulette.ROULETTE_2 -> R.drawable.bg_round_corner_6_7_ffcb14
SelectedRoulette.ROULETTE_3 -> R.drawable.bg_round_corner_6_7_ff14d9
else -> R.drawable.bg_round_corner_6_7_3bb9f1
}
)
tvSelectRoulette.setTextColor(
ContextCompat.getColor(
activity,
when (selectedRouletteLiveData.value) {
SelectedRoulette.ROULETTE_2 -> R.color.black
else -> R.color.color_eeeeee
}
)
)
}
@SuppressLint("SetTextI18n")
private fun setRouletteData(roulettePreview: RoulettePreview) {
dialogView.tvSpinRoulette.text = "${roulettePreview.can}캔으로 룰렛 돌리기"
dialogView.tvSpinRoulette.setOnClickListener { dialogView.tvSpinRoulette.setOnClickListener {
if (onClickSpin != null) { if (onClickSpin != null) {
onClickSpin!!() onClickSpin!!(roulettePreview.id)
} }
alertDialog.dismiss() alertDialog.dismiss()
} }
dialogView.tvTitle.text = title.ifBlank { "룰렛" } dialogView.tvSpinRoulette.setTextColor(
ContextCompat.getColor(
activity,
when (selectedRouletteLiveData.value) {
SelectedRoulette.ROULETTE_2 -> R.color.black
else -> R.color.white
}
)
)
dialogView.tvSpinRoulette.setBackgroundResource(
when (selectedRouletteLiveData.value) {
SelectedRoulette.ROULETTE_2 -> R.drawable.bg_round_corner_10_ffcb14
SelectedRoulette.ROULETTE_3 -> R.drawable.bg_round_corner_10_ff14d9
else -> R.drawable.bg_round_corner_10_3bb9f1
}
)
dialogView.tvTitle.text = title.ifBlank {
when (selectedRouletteLiveData.value) {
SelectedRoulette.ROULETTE_2 -> "룰렛 2"
SelectedRoulette.ROULETTE_3 -> "룰렛 3"
else -> "룰렛 1"
}
}
if (onClickSpin != null) { if (onClickSpin != null) {
dialogView.tvCan.visibility = View.VISIBLE dialogView.tvCan.visibility = View.VISIBLE
dialogView.tvCan.text = SharedPreferenceManager.can.moneyFormat() dialogView.tvCan.text = SharedPreferenceManager.can.moneyFormat()
@ -80,7 +260,8 @@ class RoulettePreviewDialog(
dialogView.tvCan.visibility = View.GONE dialogView.tvCan.visibility = View.GONE
} }
preview.items.forEachIndexed { index, item -> dialogView.llRouletteOptionContainer.removeAllViews()
roulettePreview.items.forEachIndexed { index, item ->
dialogView.llRouletteOptionContainer.addView(createOptionView(index, item)) dialogView.llRouletteOptionContainer.addView(createOptionView(index, item))
} }
} }

View File

@ -74,12 +74,10 @@ class RouletteView @JvmOverloads constructor(
override fun onDraw(canvas: Canvas) { override fun onDraw(canvas: Canvas) {
super.onDraw(canvas) super.onDraw(canvas)
val totalWeight = items.asSequence().map { it.weight }.sum()
var startAngle = -90f var startAngle = -90f
val shuffledColors = colors.shuffled() val shuffledColors = colors.shuffled()
items.forEachIndexed { index, (option, weight) -> items.forEachIndexed { index, (option, percentage) ->
val sweepAngle = (weight / totalWeight.toFloat()) * 360f val sweepAngle = (percentage / 100) * 360f
fillPaint.color = shuffledColors[index] fillPaint.color = shuffledColors[index]
canvas.drawArc(rect, startAngle, sweepAngle, true, fillPaint) canvas.drawArc(rect, startAngle, sweepAngle, true, fillPaint)
@ -117,11 +115,10 @@ class RouletteView @JvmOverloads constructor(
} }
private fun getAngleForOption(option: String): Float { private fun getAngleForOption(option: String): Float {
val totalWeight = items.asSequence().map { it.weight }.sum()
var startAngle = 0f var startAngle = 0f
items.forEach { (currentOption, weight) -> items.forEach { (currentOption, percentage) ->
val sweepAngle = (weight / totalWeight.toFloat()) * 360f val sweepAngle = (percentage / 100) * 360f
if (currentOption == option) { if (currentOption == option) {
// Return the midpoint angle of the segment // Return the midpoint angle of the segment
return (startAngle + sweepAngle / 2) return (startAngle + sweepAngle / 2)

View File

@ -4,5 +4,6 @@ import com.google.gson.annotations.SerializedName
data class SpinRouletteRequest( data class SpinRouletteRequest(
@SerializedName("roomId") val roomId: Long, @SerializedName("roomId") val roomId: Long,
@SerializedName("rouletteId") val rouletteId: Long,
@SerializedName("container") val container: String = "aos" @SerializedName("container") val container: String = "aos"
) )

View File

@ -0,0 +1,9 @@
package kr.co.vividnext.sodalive.live.roulette
import com.google.gson.annotations.SerializedName
data class SpinRouletteResponse(
@SerializedName("can") val can: Int,
@SerializedName("result") val result: String,
@SerializedName("items") val items: List<RouletteItem>
)

View File

@ -5,6 +5,7 @@ import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.live.roulette.GetNewRouletteResponse import kr.co.vividnext.sodalive.live.roulette.GetNewRouletteResponse
import kr.co.vividnext.sodalive.live.roulette.GetRouletteResponse import kr.co.vividnext.sodalive.live.roulette.GetRouletteResponse
import kr.co.vividnext.sodalive.live.roulette.SpinRouletteRequest import kr.co.vividnext.sodalive.live.roulette.SpinRouletteRequest
import kr.co.vividnext.sodalive.live.roulette.SpinRouletteResponse
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Header import retrofit2.http.Header
@ -14,37 +15,37 @@ import retrofit2.http.Path
import retrofit2.http.Query import retrofit2.http.Query
interface RouletteApi { interface RouletteApi {
@POST("/new-roulette") @POST("/v2/roulette")
fun createRoulette( fun createRoulette(
@Body request: CreateRouletteRequest, @Body request: CreateRouletteRequest,
@Header("Authorization") authHeader: String @Header("Authorization") authHeader: String
): Single<ApiResponse<Any>> ): Single<ApiResponse<Any>>
@PUT("/new-roulette") @PUT("/v2/roulette")
fun updateRoulette( fun updateRoulette(
@Body request: UpdateRouletteRequest, @Body request: UpdateRouletteRequest,
@Header("Authorization") authHeader: String @Header("Authorization") authHeader: String
): Single<ApiResponse<Any>> ): Single<ApiResponse<Any>>
@GET("/new-roulette/creator") @GET("/v2/roulette/creator")
fun getAllRoulette( fun getAllRoulette(
@Query("creatorId") creatorId: Long, @Query("creatorId") creatorId: Long,
@Header("Authorization") authHeader: String @Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetNewRouletteResponse>>> ): Single<ApiResponse<List<GetNewRouletteResponse>>>
@GET("/new-roulette") @GET("/v2/roulette")
fun getRoulette( fun getRoulette(
@Query("creatorId") creatorId: Long, @Query("creatorId") creatorId: Long,
@Header("Authorization") authHeader: String @Header("Authorization") authHeader: String
): Single<ApiResponse<GetRouletteResponse>> ): Single<ApiResponse<List<GetRouletteResponse>>>
@POST("/new-roulette/spin") @POST("/v2/roulette/spin")
fun spinRoulette( fun spinRoulette(
@Body request: SpinRouletteRequest, @Body request: SpinRouletteRequest,
@Header("Authorization") authHeader: String @Header("Authorization") authHeader: String
): Single<ApiResponse<GetRouletteResponse>> ): Single<ApiResponse<SpinRouletteResponse>>
@POST("/new-roulette/refund/{id}") @POST("/v2/roulette/refund/{id}")
fun refundRouletteDonation( fun refundRouletteDonation(
@Path("id") id: Long, @Path("id") id: Long,
@Header("Authorization") authHeader: String @Header("Authorization") authHeader: String

View File

@ -1,3 +1,3 @@
package kr.co.vividnext.sodalive.live.roulette.config package kr.co.vividnext.sodalive.live.roulette.config
data class RouletteOption(var title: String, var weight: Int, var percentage: String = "50.00") data class RouletteOption(var title: String, var percentage: String = "")

View File

@ -26,6 +26,7 @@ import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.FragmentRouletteSettingsBinding import kr.co.vividnext.sodalive.databinding.FragmentRouletteSettingsBinding
import kr.co.vividnext.sodalive.live.roulette.RoulettePreviewDialog import kr.co.vividnext.sodalive.live.roulette.RoulettePreviewDialog
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import java.util.Locale
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class RouletteSettingsFragment : BaseFragment<FragmentRouletteSettingsBinding>( class RouletteSettingsFragment : BaseFragment<FragmentRouletteSettingsBinding>(
@ -103,6 +104,7 @@ class RouletteSettingsFragment : BaseFragment<FragmentRouletteSettingsBinding>(
} }
} }
@SuppressLint("SetTextI18n")
private fun bindData() { private fun bindData() {
viewModel.selectedRouletteLiveData.observe(viewLifecycleOwner) { viewModel.selectedRouletteLiveData.observe(viewLifecycleOwner) {
deselectAllRoulette() deselectAllRoulette()
@ -156,12 +158,22 @@ class RouletteSettingsFragment : BaseFragment<FragmentRouletteSettingsBinding>(
viewModel.roulettePreviewLiveData.observe(viewLifecycleOwner) { viewModel.roulettePreviewLiveData.observe(viewLifecycleOwner) {
RoulettePreviewDialog( RoulettePreviewDialog(
activity = requireActivity(), activity = requireActivity(),
preview = it, previewList = listOf(it),
title = "룰렛 미리보기", title = "룰렛 미리보기",
layoutInflater = layoutInflater layoutInflater = layoutInflater
).show() ).show()
} }
viewModel.totalPercentageLiveData.observe(viewLifecycleOwner) {
binding.tvTotalPercentage.text = "( ${
String.format(
Locale.KOREAN,
"%.2f%%",
it
)
} )"
}
compositeDisposable.add( compositeDisposable.add(
binding.etSetPrice.textChanges().skip(1) binding.etSetPrice.textChanges().skip(1)
.debounce(100, TimeUnit.MILLISECONDS) .debounce(100, TimeUnit.MILLISECONDS)
@ -176,7 +188,7 @@ class RouletteSettingsFragment : BaseFragment<FragmentRouletteSettingsBinding>(
} }
private fun addOption() { private fun addOption() {
val newOption = RouletteOption("", 1) val newOption = RouletteOption("", "")
viewModel.addOption(newOption) viewModel.addOption(newOption)
} }
@ -199,16 +211,21 @@ class RouletteSettingsFragment : BaseFragment<FragmentRouletteSettingsBinding>(
val etOption = optionView.findViewById<EditText>(R.id.et_option) val etOption = optionView.findViewById<EditText>(R.id.et_option)
val tvOptionTitle = optionView.findViewById<TextView>(R.id.tv_option_title) val tvOptionTitle = optionView.findViewById<TextView>(R.id.tv_option_title)
val tvPercentage = optionView.findViewById<TextView>(R.id.tv_option_percentage) val etPercentage = optionView.findViewById<EditText>(R.id.et_option_percentage)
val ivMinus = optionView.findViewById<ImageView>(R.id.iv_minus)
val ivPlus = optionView.findViewById<ImageView>(R.id.iv_plus)
val tvDelete = optionView.findViewById<TextView>(R.id.tv_delete) val tvDelete = optionView.findViewById<TextView>(R.id.tv_delete)
etOption.setText(option.title) etOption.setText(option.title)
tvOptionTitle.text = "옵션 ${index + 1}" tvOptionTitle.text = "옵션 ${index + 1}"
tvPercentage.text = "${option.percentage}%"
ivMinus.setOnClickListener { viewModel.subtractWeight(index) } try {
ivPlus.setOnClickListener { viewModel.plusWeight(index) } if (option.percentage.toFloat() > 0f) {
etPercentage.setText(option.percentage)
} else {
etPercentage.setText("")
}
} catch (e: Exception) {
etPercentage.setText("")
}
if (index == 0 || index == 1) { if (index == 0 || index == 1) {
tvDelete.visibility = View.GONE tvDelete.visibility = View.GONE
@ -227,6 +244,16 @@ class RouletteSettingsFragment : BaseFragment<FragmentRouletteSettingsBinding>(
} }
) )
compositeDisposable.add(
etPercentage.textChanges().skip(1)
.debounce(100, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe {
viewModel.inputOptionPercentage(index, it.toString())
}
)
return optionView return optionView
} }
@ -248,7 +275,7 @@ class RouletteSettingsFragment : BaseFragment<FragmentRouletteSettingsBinding>(
binding.tvSelectRoulette2.setTextColor( binding.tvSelectRoulette2.setTextColor(
ContextCompat.getColor( ContextCompat.getColor(
requireContext(), requireContext(),
R.color.color_3bb9f1 R.color.color_ffcb14
) )
) )
} else { } else {
@ -256,7 +283,7 @@ class RouletteSettingsFragment : BaseFragment<FragmentRouletteSettingsBinding>(
binding.tvSelectRoulette2.setTextColor( binding.tvSelectRoulette2.setTextColor(
ContextCompat.getColor( ContextCompat.getColor(
requireContext(), requireContext(),
R.color.color_555555 R.color.color_ff14d9
) )
) )
} }
@ -286,11 +313,20 @@ class RouletteSettingsFragment : BaseFragment<FragmentRouletteSettingsBinding>(
tvSelectRoulette: TextView tvSelectRoulette: TextView
) { ) {
ivSelectRoulette.visibility = View.VISIBLE ivSelectRoulette.visibility = View.VISIBLE
llSelectRoulette.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1) llSelectRoulette.setBackgroundResource(
when (viewModel.selectedRouletteLiveData.value) {
RouletteSettingsViewModel.SelectedRoulette.ROULETTE_2 -> R.drawable.bg_round_corner_6_7_ffcb14
RouletteSettingsViewModel.SelectedRoulette.ROULETTE_3 -> R.drawable.bg_round_corner_6_7_ff14d9
else -> R.drawable.bg_round_corner_6_7_3bb9f1
}
)
tvSelectRoulette.setTextColor( tvSelectRoulette.setTextColor(
ContextCompat.getColor( ContextCompat.getColor(
requireContext(), requireContext(),
R.color.color_eeeeee when (viewModel.selectedRouletteLiveData.value) {
RouletteSettingsViewModel.SelectedRoulette.ROULETTE_2 -> R.color.black
else -> R.color.color_eeeeee
}
) )
) )
} }

View File

@ -12,7 +12,6 @@ import kr.co.vividnext.sodalive.live.roulette.RouletteItem
import kr.co.vividnext.sodalive.live.roulette.RoulettePreview import kr.co.vividnext.sodalive.live.roulette.RoulettePreview
import kr.co.vividnext.sodalive.live.roulette.RoulettePreviewItem import kr.co.vividnext.sodalive.live.roulette.RoulettePreviewItem
import kr.co.vividnext.sodalive.live.roulette.RouletteRepository import kr.co.vividnext.sodalive.live.roulette.RouletteRepository
import kotlin.math.floor
class RouletteSettingsViewModel(private val repository: RouletteRepository) : BaseViewModel() { class RouletteSettingsViewModel(private val repository: RouletteRepository) : BaseViewModel() {
@ -48,6 +47,10 @@ class RouletteSettingsViewModel(private val repository: RouletteRepository) : Ba
val selectedRouletteLiveData: LiveData<SelectedRoulette> val selectedRouletteLiveData: LiveData<SelectedRoulette>
get() = _selectedRouletteLiveData get() = _selectedRouletteLiveData
private val _totalPercentageLiveData = MutableLiveData(0f)
val totalPercentageLiveData: LiveData<Float>
get() = _totalPercentageLiveData
var can = 0 var can = 0
var isActive = false var isActive = false
private var rouletteId = 0L private var rouletteId = 0L
@ -55,31 +58,17 @@ class RouletteSettingsViewModel(private val repository: RouletteRepository) : Ba
val rouletteList = mutableListOf<GetNewRouletteResponse>() val rouletteList = mutableListOf<GetNewRouletteResponse>()
fun plusWeight(optionIndex: Int) {
val currentOption = options[optionIndex]
options[optionIndex] = currentOption.copy(weight = currentOption.weight + 1)
recalculatePercentages(options)
}
fun subtractWeight(optionIndex: Int) {
if (options[optionIndex].weight > 1) {
val currentOption = options[optionIndex]
options[optionIndex] = currentOption.copy(weight = currentOption.weight - 1)
recalculatePercentages(options)
}
}
fun addOption(newOption: RouletteOption) { fun addOption(newOption: RouletteOption) {
if (options.size >= 10) return if (options.size >= 10) return
options.add(newOption) options.add(newOption)
recalculatePercentages(options) _optionsLiveData.value = options
calculateTotalPercentage()
} }
fun deleteOption(index: Int) { fun deleteOption(index: Int) {
val updatedOptions = options.filterIndexed { currentIndex, _ -> currentIndex != index } val updatedOptions = options.filterIndexed { currentIndex, _ -> currentIndex != index }
removeAllAndAddOptions(updatedOptions) removeAllAndAddOptions(updatedOptions)
recalculatePercentages(updatedOptions) _optionsLiveData.value = updatedOptions
} }
fun inputOption(optionIndex: Int, title: String) { fun inputOption(optionIndex: Int, title: String) {
@ -87,15 +76,10 @@ class RouletteSettingsViewModel(private val repository: RouletteRepository) : Ba
options[optionIndex] = currentOption.copy(title = title) options[optionIndex] = currentOption.copy(title = title)
} }
private fun recalculatePercentages(options: List<RouletteOption>) { fun inputOptionPercentage(optionIndex: Int, percentage: String) {
val totalWeight = options.sumOf { it.weight } val currentOption = options[optionIndex]
val updatedOptions = options.asSequence().map { option -> options[optionIndex] = currentOption.copy(percentage = percentage)
val percent = floor(option.weight.toDouble() / totalWeight * 10000) / 100 calculateTotalPercentage()
option.copy(percentage = String.format("%.2f", percent))
}.toList()
removeAllAndAddOptions(updatedOptions)
_optionsLiveData.value = updatedOptions
} }
fun toggleIsActive() { fun toggleIsActive() {
@ -107,17 +91,20 @@ class RouletteSettingsViewModel(private val repository: RouletteRepository) : Ba
_isLoading.value = true _isLoading.value = true
val items = mutableListOf<RoulettePreviewItem>() val items = mutableListOf<RoulettePreviewItem>()
for (option in options) { if (validationOptions()) {
if (option.title.trim().isEmpty()) { for (option in options) {
_toastLiveData.value = "옵션은 빈칸을 할 수 없습니다." if (option.title.trim().isEmpty()) {
_isLoading.value = false _toastLiveData.value = "옵션은 빈칸을 할 수 없습니다."
return _isLoading.value = false
return
}
items.add(RoulettePreviewItem(option.title, "${option.percentage}%"))
} }
items.add(RoulettePreviewItem(option.title, "${option.percentage}%")) _roulettePreviewLiveData.postValue(RoulettePreview(0, can, items))
} }
_roulettePreviewLiveData.postValue(RoulettePreview(can, items))
_isLoading.value = false _isLoading.value = false
} }
@ -125,24 +112,41 @@ class RouletteSettingsViewModel(private val repository: RouletteRepository) : Ba
if (!_isLoading.value!!) { if (!_isLoading.value!!) {
_isLoading.value = true _isLoading.value = true
if (rouletteId > 0) { if (validationOptions()) {
updateRoulette(onSuccess) if (rouletteId > 0) {
} else { updateRoulette(onSuccess)
createRoulette(onSuccess) } else {
createRoulette(onSuccess)
}
} }
} }
} }
private fun validationOptions(): Boolean {
var totalPercentage = 0f
for (option in options) {
if (option.title.trim().isEmpty()) {
_toastLiveData.value = "옵션은 빈칸을 할 수 없습니다."
_isLoading.value = false
return false
}
totalPercentage += option.percentage.toFloat()
}
if (totalPercentage != 100.0f) {
_toastLiveData.value = "확률이 100%가 아닙니다"
_isLoading.value = false
return false
}
return true
}
private fun updateRoulette(onSuccess: (Boolean) -> Unit) { private fun updateRoulette(onSuccess: (Boolean) -> Unit) {
val items = mutableListOf<RouletteItem>() val items = mutableListOf<RouletteItem>()
for (option in options) { for (option in options) {
if (option.title.trim().isEmpty()) { items.add(RouletteItem(title = option.title, percentage = option.percentage.toFloat()))
_toastLiveData.value = "옵션은 빈칸을 할 수 없습니다."
_isLoading.value = false
return
}
items.add(RouletteItem(title = option.title, weight = option.weight))
} }
val selectedRoulette = rouletteList[_selectedRouletteLiveData.value!!.ordinal] val selectedRoulette = rouletteList[_selectedRouletteLiveData.value!!.ordinal]
@ -169,24 +173,10 @@ class RouletteSettingsViewModel(private val repository: RouletteRepository) : Ba
SelectedRoulette.ROULETTE_3 -> "룰렛 3" SelectedRoulette.ROULETTE_3 -> "룰렛 3"
} }
var isAllActive = false
rouletteList
.filter {
it.id != selectedRoulette.id
}
.forEach {
if (it.isActive) {
isAllActive = true
}
}
val successMessage = if (isActive) { val successMessage = if (isActive) {
"${selectedRouletteTitle}로 설정하였습니다." "${selectedRouletteTitle}을 활성화 했습니다."
} else if (!isAllActive) {
"${selectedRouletteTitle}이 비활성화 되었습니다."
} else { } else {
"${selectedRouletteTitle}설정했습니다." "${selectedRouletteTitle}을 비활성화 했습니다."
} }
compositeDisposable.add( compositeDisposable.add(
@ -224,13 +214,7 @@ class RouletteSettingsViewModel(private val repository: RouletteRepository) : Ba
private fun createRoulette(onSuccess: (Boolean) -> Unit) { private fun createRoulette(onSuccess: (Boolean) -> Unit) {
val items = mutableListOf<RouletteItem>() val items = mutableListOf<RouletteItem>()
for (option in options) { for (option in options) {
if (option.title.trim().isEmpty()) { items.add(RouletteItem(title = option.title, percentage = option.percentage.toFloat()))
_toastLiveData.value = "옵션은 빈칸을 할 수 없습니다."
_isLoading.value = false
return
}
items.add(RouletteItem(title = option.title, weight = option.weight))
} }
val request = CreateRouletteRequest( val request = CreateRouletteRequest(
@ -348,10 +332,10 @@ class RouletteSettingsViewModel(private val repository: RouletteRepository) : Ba
isActive = roulette.isActive isActive = roulette.isActive
val options = roulette.items.asSequence().map { item -> val options = roulette.items.asSequence().map { item ->
RouletteOption(title = item.title, weight = item.weight) RouletteOption(title = item.title, percentage = item.percentage.toString())
}.toList() }.toList()
removeAllAndAddOptions(options = options) removeAllAndAddOptions(options = options)
recalculatePercentages(options) _optionsLiveData.value = options
} else { } else {
_canLiveData.value = 0 _canLiveData.value = 0
_isActiveLiveData.value = false _isActiveLiveData.value = false
@ -361,15 +345,30 @@ class RouletteSettingsViewModel(private val repository: RouletteRepository) : Ba
isActive = false isActive = false
options.clear() options.clear()
options.add(RouletteOption(title = "", weight = 1)) options.add(RouletteOption(title = "", percentage = ""))
options.add(RouletteOption(title = "", weight = 1)) options.add(RouletteOption(title = "", percentage = ""))
recalculatePercentages(options) _optionsLiveData.value = options
} }
calculateTotalPercentage()
} }
} }
private fun removeAllAndAddOptions(options: List<RouletteOption>) { private fun removeAllAndAddOptions(options: List<RouletteOption>) {
this.options.clear() this.options.clear()
this.options.addAll(options) this.options.addAll(options)
calculateTotalPercentage()
}
private fun calculateTotalPercentage() {
val totalPercent = options.map {
try {
it.percentage.toFloat()
} catch (e: Exception) {
0f
}
}.sum()
_totalPercentageLiveData.value = totalPercent
} }
} }

View File

@ -10,7 +10,6 @@ import com.google.android.material.tabs.TabLayout
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseActivity import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.Constants import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ActivityCanChargeBinding import kr.co.vividnext.sodalive.databinding.ActivityCanChargeBinding
import kr.co.vividnext.sodalive.mypage.can.charge.iap.CanChargeIapFragment import kr.co.vividnext.sodalive.mypage.can.charge.iap.CanChargeIapFragment
import kr.co.vividnext.sodalive.mypage.can.charge.pg.CanChargePgFragment import kr.co.vividnext.sodalive.mypage.can.charge.pg.CanChargePgFragment
@ -42,7 +41,10 @@ class CanChargeActivity : BaseActivity<ActivityCanChargeBinding>(
) )
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction()
.replace(R.id.fl_container, CanChargeIapFragment()).commit() .replace(
R.id.fl_container,
CanChargePgFragment()
).commit()
} }
override fun setupView() { override fun setupView() {
@ -56,33 +58,29 @@ class CanChargeActivity : BaseActivity<ActivityCanChargeBinding>(
} }
private fun setupTabs() { private fun setupTabs() {
if (SharedPreferenceManager.isAuth) { val tabs = binding.tabs
val tabs = binding.tabs
tabs.visibility = View.VISIBLE tabs.visibility = View.VISIBLE
tabs.addTab(tabs.newTab().setText("인 앱 결제")) tabs.addTab(tabs.newTab().setText("PG"))
tabs.addTab(tabs.newTab().setText("PG")) tabs.addTab(tabs.newTab().setText("인 앱 결제"))
tabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { tabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) { override fun onTabSelected(tab: TabLayout.Tab) {
when (tab.position) { when (tab.position) {
0 -> supportFragmentManager.beginTransaction() 0 -> supportFragmentManager.beginTransaction()
.replace(R.id.fl_container, CanChargeIapFragment()).commit() .replace(R.id.fl_container, CanChargePgFragment()).commit()
1 -> supportFragmentManager.beginTransaction() 1 -> supportFragmentManager.beginTransaction()
.replace(R.id.fl_container, CanChargePgFragment()).commit() .replace(R.id.fl_container, CanChargeIapFragment()).commit()
}
} }
}
override fun onTabUnselected(tab: TabLayout.Tab) { override fun onTabUnselected(tab: TabLayout.Tab) {
} }
override fun onTabReselected(tab: TabLayout.Tab) { override fun onTabReselected(tab: TabLayout.Tab) {
} }
}) })
} else {
binding.tabs.visibility = View.GONE
}
} }
fun selectCan(model: CanResponse) { fun selectCan(model: CanResponse) {

View File

@ -1,7 +1,6 @@
package kr.co.vividnext.sodalive.mypage.can.charge.iap package kr.co.vividnext.sodalive.mypage.can.charge.iap
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Rect import android.graphics.Rect
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
@ -42,10 +41,6 @@ class CanChargeIapFragment : BaseFragment<FragmentCanChargeIapBinding>(
private lateinit var purchaseUpdateListener: PurchasesUpdatedListener private lateinit var purchaseUpdateListener: PurchasesUpdatedListener
fun safeContext(): Context? {
return if (isAdded) context else null
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@ -56,29 +51,9 @@ class CanChargeIapFragment : BaseFragment<FragmentCanChargeIapBinding>(
setupBillingClient() setupBillingClient()
} }
override fun onStart() { override fun onDestroy() {
super.onStart()
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingServiceDisconnected() {
viewModel.showToast("인 앱 결제 이용이 불가능 합니다. 다시 시도해 주세요.")
viewModel.setLoading(false)
}
override fun onBillingSetupFinished(billingResult: BillingResult) {
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
queryAndConsumeUnconsumedPurchases()
} else {
viewModel.showToast("인 앱 결제 이용이 불가능 합니다. 다시 시도해 주세요.")
viewModel.setLoading(false)
}
}
})
}
override fun onStop() {
super.onStop()
billingClient.endConnection() billingClient.endConnection()
super.onDestroy()
} }
private fun bindData() { private fun bindData() {
@ -171,6 +146,22 @@ class CanChargeIapFragment : BaseFragment<FragmentCanChargeIapBinding>(
.build() .build()
loadingDialog.show(screenWidth) loadingDialog.show(screenWidth)
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingServiceDisconnected() {
viewModel.showToast("인 앱 결제 이용이 불가능 합니다. 다시 시도해 주세요.")
viewModel.setLoading(false)
}
override fun onBillingSetupFinished(billingResult: BillingResult) {
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
queryAndConsumeUnconsumedPurchases()
} else {
viewModel.showToast("인 앱 결제 이용이 불가능 합니다. 다시 시도해 주세요.")
viewModel.setLoading(false)
}
}
})
} }
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
@ -219,7 +210,9 @@ class CanChargeIapFragment : BaseFragment<FragmentCanChargeIapBinding>(
if (purchaseList.isNotEmpty()) { if (purchaseList.isNotEmpty()) {
for (purchase in purchaseList) { for (purchase in purchaseList) {
if (!purchase.isAcknowledged) { if (!purchase.isAcknowledged) {
consumePurchase(purchase) consumePurchase(purchase) {
queryAvailableCans()
}
} }
} }
} else { } else {
@ -229,14 +222,14 @@ class CanChargeIapFragment : BaseFragment<FragmentCanChargeIapBinding>(
} }
} }
private fun consumePurchase(purchase: Purchase) { private fun consumePurchase(purchase: Purchase, onSuccess: () -> Unit) {
val params = ConsumeParams.newBuilder() val params = ConsumeParams.newBuilder()
.setPurchaseToken(purchase.purchaseToken) .setPurchaseToken(purchase.purchaseToken)
.build() .build()
billingClient.consumeAsync(params) { billingResult, _ -> billingClient.consumeAsync(params) { billingResult, _ ->
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) { if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
queryAvailableCans() onSuccess()
} }
} }
} }

View File

@ -0,0 +1,237 @@
package kr.co.vividnext.sodalive.mypage.can.payment
import android.annotation.SuppressLint
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.View
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import com.google.gson.Gson
import com.orhanobut.logger.Logger
import kr.co.bootpay.android.Bootpay
import kr.co.bootpay.android.events.BootpayEventListener
import kr.co.bootpay.android.models.BootUser
import kr.co.bootpay.android.models.Payload
import kr.co.vividnext.sodalive.BuildConfig
import kr.co.vividnext.sodalive.R
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.ActivityCanPaymentBinding
import kr.co.vividnext.sodalive.extensions.fontSpan
import kr.co.vividnext.sodalive.extensions.moneyFormat
import kr.co.vividnext.sodalive.mypage.auth.BootpayResponse
import kr.co.vividnext.sodalive.mypage.can.charge.pg.VerifyRequest
import org.koin.android.ext.android.inject
class CanPaymentTempActivity : BaseActivity<ActivityCanPaymentBinding>(
ActivityCanPaymentBinding::inflate
) {
enum class PaymentMethod(val method: String) {
CARD("카드"), BANK("계좌이체"), PHONE("휴대폰")
}
private val viewModel: CanPaymentTempViewModel by inject()
private val handler = Handler(Looper.getMainLooper())
private lateinit var loadingDialog: LoadingDialog
private lateinit var title: String
private var can: Int = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
bindData()
}
@SuppressLint("SetTextI18n")
override fun setupView() {
title = intent.getStringExtra("title") ?: ""
can = intent.getIntExtra("can", 0)
loadingDialog = LoadingDialog(this, layoutInflater)
if (title.isBlank() || can <= 0) {
Toast.makeText(
applicationContext,
"다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다.",
Toast.LENGTH_LONG
).show()
finish()
}
binding.toolbar.tvBack.text = "결제하기"
binding.toolbar.tvBack.setOnClickListener { finish() }
binding.ivCan.visibility = View.GONE
binding.tvAlert.visibility = View.GONE
binding.tvChargeCanTitle.text = title
binding.tvPrice.text = (can * 110).moneyFormat()
binding.tvPaymentPrice.text = "${(can * 110).moneyFormat()}".fontSpan(
ResourcesCompat.getFont(applicationContext, R.font.gmarket_sans_light),
""
)
binding.tvAgree.setOnClickListener {
binding.tvAgree.isSelected = !binding.tvAgree.isSelected
}
binding.tvPayment.setOnClickListener {
if (viewModel.paymentMethodLiveData.value == null) {
Toast.makeText(
applicationContext,
"결제수단을 선택해 주세요.",
Toast.LENGTH_LONG
).show()
return@setOnClickListener
}
if (!binding.tvAgree.isSelected) {
Toast.makeText(
applicationContext,
"결제 진행에 동의하셔야 결제가 가능합니다.",
Toast.LENGTH_LONG
).show()
return@setOnClickListener
}
requestCharge()
}
binding.tvMethodCard.setOnClickListener { viewModel.setPaymentMethod(PaymentMethod.CARD) }
binding.tvMethodBank.setOnClickListener { viewModel.setPaymentMethod(PaymentMethod.BANK) }
binding.tvMethodPhone.setOnClickListener { viewModel.setPaymentMethod(PaymentMethod.PHONE) }
}
private fun allPaymentMethodSelectFalse() {
paymentMethodSelectFalse(binding.tvMethodBank)
paymentMethodSelectFalse(binding.tvMethodCard)
paymentMethodSelectFalse(binding.tvMethodPhone)
}
private fun paymentMethodSelectFalse(view: TextView) {
view.typeface = ResourcesCompat.getFont(
applicationContext,
R.font.gmarket_sans_medium
)
view.setTextColor(ContextCompat.getColor(applicationContext, R.color.color_eeeeee))
view.setBackgroundResource(R.drawable.bg_round_corner_10_232323_777777)
}
private fun paymentMethodSelect(view: TextView) {
view.typeface = ResourcesCompat.getFont(
applicationContext,
R.font.gmarket_sans_bold
)
view.setTextColor(ContextCompat.getColor(applicationContext, R.color.color_3bb9f1))
view.setBackgroundResource(R.drawable.bg_round_corner_10_13181b_3bb9f1)
}
private fun requestCharge() {
viewModel.chargeCan(
can = can,
paymentGateway = PaymentGateway.PG,
onSuccess = {
requestPayment(chargeId = it)
},
onFailure = {
Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show()
}
)
}
private fun requestPayment(chargeId: Long) {
val user = BootUser()
.setId("${SharedPreferenceManager.userId}")
.setUsername(SharedPreferenceManager.nickname)
val payload = Payload()
.setApplicationId(BuildConfig.BOOTPAY_APP_ID)
.setOrderId("$chargeId")
.setOrderName(title)
.setPrice((can * 110).toDouble())
.setTaxFree(0.toDouble())
.setPg("세틀뱅크")
.setMethod(viewModel.paymentMethodLiveData.value!!.method)
.setUser(user)
Bootpay.init(this, this)
.setPayload(payload)
.setEventListener(object : BootpayEventListener {
override fun onCancel(data: String) {
Logger.e("onCancel: $data")
}
override fun onError(data: String) {
Logger.e("onError: $data")
Toast.makeText(applicationContext, data, Toast.LENGTH_LONG).show()
}
override fun onClose() {
Logger.e("onClose")
Bootpay.removePaymentWindow()
}
override fun onIssued(data: String) {
Logger.e("onIssued: $data")
}
override fun onConfirm(data: String): Boolean {
Logger.e("onConfirm: $data")
return true
}
override fun onDone(data: String) {
Logger.e("onDone: $data")
handler.post {
verifyPayment(data)
Bootpay.removePaymentWindow()
}
}
}).requestPayment()
}
private fun verifyPayment(data: String) {
val bootpayResponse = Gson().fromJson(data, BootpayResponse::class.java)
val request = VerifyRequest(bootpayResponse.data.receiptId, bootpayResponse.data.orderId)
viewModel.verify(
request,
onSuccess = {
SharedPreferenceManager.can += can
setResult(RESULT_OK)
finish()
},
onFailure = {
Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show()
}
)
}
private fun bindData() {
viewModel.paymentMethodLiveData.observe(this) {
allPaymentMethodSelectFalse()
if (it != null) {
when (it) {
PaymentMethod.CARD -> paymentMethodSelect(binding.tvMethodCard)
PaymentMethod.BANK -> paymentMethodSelect(binding.tvMethodBank)
PaymentMethod.PHONE -> paymentMethodSelect(binding.tvMethodPhone)
}
}
}
viewModel.isLoading.observe(this) {
if (it) {
loadingDialog.show(screenWidth, "")
} else {
loadingDialog.dismiss()
}
}
}
}

View File

@ -0,0 +1,15 @@
package kr.co.vividnext.sodalive.mypage.can.payment
import kr.co.vividnext.sodalive.mypage.can.charge.pg.VerifyRequest
class CanPaymentTempRepository(private val api: CanTempApi) {
fun chargeCan(
request: ChargeTempRequest,
token: String
) = api.chargeCan(request, authHeader = token)
fun verify(
request: VerifyRequest,
token: String
) = api.verifyCharge(request, authHeader = token)
}

View File

@ -0,0 +1,88 @@
package kr.co.vividnext.sodalive.mypage.can.payment
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.mypage.can.charge.pg.VerifyRequest
class CanPaymentTempViewModel(private val repository: CanPaymentTempRepository) : BaseViewModel() {
private val _paymentMethodLiveData = MutableLiveData<CanPaymentTempActivity.PaymentMethod?>()
val paymentMethodLiveData: LiveData<CanPaymentTempActivity.PaymentMethod?>
get() = _paymentMethodLiveData
private var _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean>
get() = _isLoading
fun chargeCan(
can: Int,
paymentGateway: PaymentGateway,
onSuccess: (Long) -> Unit,
onFailure: (String) -> Unit
) {
_isLoading.value = true
val request = ChargeTempRequest(can, can * 110, paymentGateway)
compositeDisposable.add(
repository.chargeCan(request, "Bearer ${SharedPreferenceManager.token}")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success && it.data != null) {
onSuccess(it.data.chargeId)
} else {
if (it.message != null) {
onFailure(it.message)
} else {
onFailure("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
onFailure("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
fun verify(request: VerifyRequest, onSuccess: () -> Unit, onFailure: (String) -> Unit) {
_isLoading.value = true
compositeDisposable.add(
repository.verify(
request = request,
"Bearer ${SharedPreferenceManager.token}"
).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success) {
onSuccess()
} else {
if (it.message != null) {
onFailure(it.message)
} else {
onFailure("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
onFailure("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
fun setPaymentMethod(paymentMethod: CanPaymentTempActivity.PaymentMethod) {
_paymentMethodLiveData.value = paymentMethod
}
}

View File

@ -0,0 +1,30 @@
package kr.co.vividnext.sodalive.mypage.can.payment
import com.google.gson.annotations.SerializedName
import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.mypage.can.charge.pg.ChargeResponse
import kr.co.vividnext.sodalive.mypage.can.charge.pg.VerifyRequest
import retrofit2.http.Body
import retrofit2.http.Header
import retrofit2.http.POST
interface CanTempApi {
@POST("/charge/temp")
fun chargeCan(
@Body chargeRequest: ChargeTempRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<ChargeResponse>>
@POST("/charge/temp/verify")
fun verifyCharge(
@Body request: VerifyRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
}
data class ChargeTempRequest(
@SerializedName("can") val can: Int,
@SerializedName("price") val price: Int,
@SerializedName("paymentGateway") val paymentGateway: PaymentGateway
)

View File

@ -23,7 +23,7 @@ class MemberTagAdapter(
fun bind(item: MemberTagResponse) { fun bind(item: MemberTagResponse) {
if (selectedTags.contains(item.tag)) { if (selectedTags.contains(item.tag)) {
binding.ivTagChecked.visibility = View.VISIBLE binding.ivTagChecked.visibility = View.VISIBLE
binding.ivTag.setBackgroundResource(R.drawable.bg_round_corner_30_9970ff) binding.ivTag.setBackgroundResource(R.drawable.bg_round_corner_30_3bb9f1)
isChecked = true isChecked = true
} else { } else {
binding.ivTagChecked.visibility = View.GONE binding.ivTagChecked.visibility = View.GONE
@ -44,7 +44,7 @@ class MemberTagAdapter(
if (onItemClick(item.tag, isChecked)) { if (onItemClick(item.tag, isChecked)) {
if (isChecked) { if (isChecked) {
binding.ivTagChecked.visibility = View.VISIBLE binding.ivTagChecked.visibility = View.VISIBLE
binding.ivTag.setBackgroundResource(R.drawable.bg_round_corner_30_9970ff) binding.ivTag.setBackgroundResource(R.drawable.bg_round_corner_30_3bb9f1)
} else { } else {
binding.ivTagChecked.visibility = View.GONE binding.ivTagChecked.visibility = View.GONE
binding.ivTag.background = null binding.ivTag.background = null

View File

@ -34,8 +34,13 @@ class SplashActivity : BaseActivity<ActivitySplashBinding>(ActivitySplashBinding
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val lp = binding.ivText.layoutParams as ConstraintLayout.LayoutParams val lp = binding.ivText.layoutParams as ConstraintLayout.LayoutParams
lp.topMargin = screenHeight * 787 / 2337 lp.topMargin = screenHeight * 302 / 2337
binding.ivText.layoutParams = lp binding.ivText.layoutParams = lp
val lp2 = binding.ivText2.layoutParams as ConstraintLayout.LayoutParams
lp2.bottomMargin = screenHeight * 195 / 2337
binding.ivText2.layoutParams = lp2
setupRemoteConfig() setupRemoteConfig()
fetchAndroidLatestVersion() fetchAndroidLatestVersion()
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 996 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 886 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/color_ff14d9" />
<corners android:radius="10dp" />
<stroke
android:width="1dp"
android:color="@color/color_ff14d9" />
</shape>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/color_ffcb14" />
<corners android:radius="10dp" />
<stroke
android:width="1dp"
android:color="@color/color_ffcb14" />
</shape>

View File

@ -4,5 +4,5 @@
<corners android:radius="10dp" /> <corners android:radius="10dp" />
<stroke <stroke
android:width="1dp" android:width="1dp"
android:color="@color/color_9970ff" /> android:color="@color/color_ff14d9" />
</shape> </shape>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@android:color/transparent" />
<corners android:radius="10dp" />
<stroke
android:width="1dp"
android:color="@color/color_ffcb14" />
</shape>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/color_002abd" />
<corners android:radius="13.3dp" />
<stroke
android:width="1dp"
android:color="@color/color_002abd" />
</shape>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/color_3bb9f1" />
<corners android:radius="13.3dp" />
<stroke
android:width="1dp"
android:color="@color/color_3bb9f1" />
</shape>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/color_ec6033" />
<corners android:radius="13.3dp" />
<stroke
android:width="1dp"
android:color="@color/color_ec6033" />
</shape>

View File

@ -4,5 +4,5 @@
<corners android:radius="16.7dp" /> <corners android:radius="16.7dp" />
<stroke <stroke
android:width="1dp" android:width="1dp"
android:color="@color/color_9970ff" /> android:color="@color/color_3bb9f1" />
</shape> </shape>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/color_222222" />
<corners android:radius="26.7dp" />
<stroke android:width="0dp" />
</shape>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@android:color/transparent" />
<corners android:radius="26.7dp" />
<stroke
android:width="1dp"
android:color="@color/color_909090" />
</shape>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/color_312827" />
<corners android:radius="2.6dp" />
<stroke
android:width="1dp"
android:color="@color/color_312827" />
</shape>

View File

@ -3,5 +3,5 @@
<corners android:radius="30dp" /> <corners android:radius="30dp" />
<stroke <stroke
android:width="1dp" android:width="1dp"
android:color="@color/color_9970ff" /> android:color="@color/color_3bb9f1" />
</shape> </shape>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/color_ccc25264" />
<corners android:radius="5.3dp" />
<stroke
android:width="1dp"
android:color="@color/color_ccc25264" />
</shape>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/color_ff14d9" />
<corners android:radius="6.7dp" />
<stroke
android:width="1dp"
android:color="@color/color_ff14d9" />
</shape>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/color_ffcb14" />
<corners android:radius="6.7dp" />
<stroke
android:width="1dp"
android:color="@color/color_ffcb14" />
</shape>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/color_111111" />
<corners
android:topLeftRadius="21.3dp"
android:topRightRadius="21.3dp" />
<stroke
android:width="1dp"
android:color="@color/color_111111" />
</shape>

View File

@ -295,6 +295,7 @@
tools:text="매버릭 팔레트 (feat. J-DRAGON)" /> tools:text="매버릭 팔레트 (feat. J-DRAGON)" />
<ScrollView <ScrollView
android:id="@+id/sv_action_buttons"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="13.3dp"> android:layout_marginTop="13.3dp">
@ -545,6 +546,7 @@
android:visibility="gone"> android:visibility="gone">
<ImageView <ImageView
android:id="@+id/iv_can"
android:layout_width="16.7dp" android:layout_width="16.7dp"
android:layout_height="16.7dp" android:layout_height="16.7dp"
android:contentDescription="@null" android:contentDescription="@null"
@ -561,6 +563,7 @@
tools:text="300" /> tools:text="300" />
<TextView <TextView
android:id="@+id/tv_unit"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fontFamily="@font/gmarket_sans_light" android:fontFamily="@font/gmarket_sans_light"

View File

@ -51,7 +51,7 @@
android:layout_width="106.7dp" android:layout_width="106.7dp"
android:layout_height="106.7dp" android:layout_height="106.7dp"
android:adjustViewBounds="true" android:adjustViewBounds="true"
android:background="@drawable/bg_round_corner_13_3_3e3358" android:background="@drawable/bg_round_corner_13_3_13181b"
android:contentDescription="@null" android:contentDescription="@null"
android:padding="13.3dp" android:padding="13.3dp"
android:src="@drawable/ic_logo" /> android:src="@drawable/ic_logo" />
@ -209,7 +209,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:background="@drawable/bg_round_corner_6_7_1f1734" android:background="@drawable/bg_round_corner_6_7_13181b"
android:gravity="center" android:gravity="center"
android:paddingVertical="14.3dp"> android:paddingVertical="14.3dp">
@ -228,7 +228,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fontFamily="@font/gmarket_sans_bold" android:fontFamily="@font/gmarket_sans_bold"
android:text="전체 연령" android:text="전체 연령"
android:textColor="@color/color_9970ff" android:textColor="@color/color_3bb9f1"
android:textSize="14.7sp" /> android:textSize="14.7sp" />
</LinearLayout> </LinearLayout>
@ -238,7 +238,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="13.3dp" android:layout_marginStart="13.3dp"
android:layout_weight="1" android:layout_weight="1"
android:background="@drawable/bg_round_corner_6_7_1f1734" android:background="@drawable/bg_round_corner_6_7_13181b"
android:gravity="center" android:gravity="center"
android:paddingVertical="14.3dp"> android:paddingVertical="14.3dp">
@ -257,7 +257,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fontFamily="@font/gmarket_sans_bold" android:fontFamily="@font/gmarket_sans_bold"
android:text="19세 이상" android:text="19세 이상"
android:textColor="@color/color_9970ff" android:textColor="@color/color_3bb9f1"
android:textSize="14.7sp" /> android:textSize="14.7sp" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
@ -300,7 +300,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:background="@drawable/bg_round_corner_6_7_1f1734" android:background="@drawable/bg_round_corner_6_7_13181b"
android:gravity="center" android:gravity="center"
android:paddingVertical="14.3dp"> android:paddingVertical="14.3dp">
@ -319,7 +319,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fontFamily="@font/gmarket_sans_bold" android:fontFamily="@font/gmarket_sans_bold"
android:text="댓글 가능" android:text="댓글 가능"
android:textColor="@color/color_9970ff" android:textColor="@color/color_3bb9f1"
android:textSize="14.7sp" /> android:textSize="14.7sp" />
</LinearLayout> </LinearLayout>
@ -329,7 +329,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="13.3dp" android:layout_marginStart="13.3dp"
android:layout_weight="1" android:layout_weight="1"
android:background="@drawable/bg_round_corner_6_7_1f1734" android:background="@drawable/bg_round_corner_6_7_13181b"
android:gravity="center" android:gravity="center"
android:paddingVertical="14.3dp"> android:paddingVertical="14.3dp">
@ -348,7 +348,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fontFamily="@font/gmarket_sans_bold" android:fontFamily="@font/gmarket_sans_bold"
android:text="댓글 불가" android:text="댓글 불가"
android:textColor="@color/color_9970ff" android:textColor="@color/color_3bb9f1"
android:textSize="14.7sp" /> android:textSize="14.7sp" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
@ -368,11 +368,11 @@
android:layout_height="50dp" android:layout_height="50dp"
android:layout_marginEnd="6.7dp" android:layout_marginEnd="6.7dp"
android:layout_weight="1" android:layout_weight="1"
android:background="@drawable/bg_round_corner_6_7_1f1734_9970ff" android:background="@drawable/bg_round_corner_6_7_13181b_3bb9f1"
android:fontFamily="@font/gmarket_sans_bold" android:fontFamily="@font/gmarket_sans_bold"
android:gravity="center" android:gravity="center"
android:text="취소" android:text="취소"
android:textColor="@color/color_9970ff" android:textColor="@color/color_3bb9f1"
android:textSize="18.3sp" /> android:textSize="18.3sp" />
<TextView <TextView
@ -381,7 +381,7 @@
android:layout_height="50dp" android:layout_height="50dp"
android:layout_marginStart="6.7dp" android:layout_marginStart="6.7dp"
android:layout_weight="1" android:layout_weight="1"
android:background="@drawable/bg_round_corner_6_7_9970ff" android:background="@drawable/bg_round_corner_6_7_3bb9f1"
android:fontFamily="@font/gmarket_sans_bold" android:fontFamily="@font/gmarket_sans_bold"
android:gravity="center" android:gravity="center"
android:text="수정" android:text="수정"

View File

@ -33,29 +33,32 @@
android:paddingHorizontal="13.3dp" android:paddingHorizontal="13.3dp"
android:paddingVertical="23.3dp"> android:paddingVertical="23.3dp">
<ImageView <LinearLayout
android:id="@+id/ic_can"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:contentDescription="@null" android:orientation="horizontal"
android:src="@drawable/ic_can"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent">
<TextView <ImageView
android:id="@+id/tv_charge_can_title" android:id="@+id/iv_can"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="13.3dp" android:layout_marginEnd="13.3dp"
android:fontFamily="@font/gmarket_sans_bold" android:contentDescription="@null"
android:gravity="center" android:src="@drawable/ic_can" />
android:textColor="@color/color_eeeeee"
android:textSize="15.3sp" <TextView
app:layout_constraintBottom_toBottomOf="@+id/ic_can" android:id="@+id/tv_charge_can_title"
app:layout_constraintStart_toEndOf="@+id/ic_can" android:layout_width="wrap_content"
app:layout_constraintTop_toTopOf="@+id/ic_can" android:layout_height="wrap_content"
tools:text="5000 캔 + 1000 캔" /> android:fontFamily="@font/gmarket_sans_bold"
android:gravity="center"
android:textColor="@color/color_eeeeee"
android:textSize="15.3sp"
tools:text="5000 캔 + 1000 캔" />
</LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/ll_price" android:id="@+id/ll_price"
@ -171,6 +174,7 @@
app:drawableStartCompat="@drawable/ic_select" /> app:drawableStartCompat="@drawable/ic_select" />
<TextView <TextView
android:id="@+id/tv_alert"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="26.7dp" android:layout_marginHorizontal="26.7dp"

View File

@ -88,7 +88,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="13.3dp" android:layout_marginHorizontal="13.3dp"
android:layout_marginTop="60dp" android:layout_marginTop="60dp"
android:background="@drawable/bg_round_corner_6_7_9970ff" android:background="@drawable/bg_round_corner_6_7_3bb9f1"
android:fontFamily="@font/gmarket_sans_bold" android:fontFamily="@font/gmarket_sans_bold"
android:gravity="center" android:gravity="center"
android:paddingVertical="16dp" android:paddingVertical="16dp"
@ -102,13 +102,13 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:layout_marginTop="93dp" android:layout_marginTop="93dp"
android:background="@drawable/bg_round_corner_8_transparent_9970ff" android:background="@drawable/bg_round_corner_8_transparent_3bb9f1"
android:drawablePadding="13.3dp" android:drawablePadding="13.3dp"
android:paddingHorizontal="18.7dp" android:paddingHorizontal="18.7dp"
android:paddingVertical="10.7dp" android:paddingVertical="10.7dp"
android:text="고객센터로 문의하기" android:text="고객센터로 문의하기"
android:textColor="@color/color_9970ff" android:textColor="@color/color_3bb9f1"
app:drawableStartCompat="@drawable/ic_headphones_purple" /> app:drawableStartCompat="@drawable/ic_headphones_blue" />
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -29,9 +29,10 @@
android:layout_gravity="center" android:layout_gravity="center"
android:layout_marginTop="28.3dp" android:layout_marginTop="28.3dp"
android:fontFamily="@font/gmarket_sans_medium" android:fontFamily="@font/gmarket_sans_medium"
android:text="🙀현재 참여 가능한 라이브 방송이 없거나\n연령제한으로 입장이 불가능합니다.\n본인인증을 해보거나 채널을 팔로잉하고\n라이브 방송 알림을 받아보세요." android:text="현재 참여 가능한 라이브 방송이 없거나\n연령제한으로 입장이 불가능합니다.\n본인인증을 해보거나 채널을 팔로잉하고\n라이브 방송 알림을 받아보세요."
android:textColor="@color/color_bbbbbb" android:textColor="@color/color_bbbbbb"
android:textSize="15sp" android:textSize="13sp"
android:lineSpacingExtra="8dp"
android:visibility="gone" /> android:visibility="gone" />
</LinearLayout> </LinearLayout>

View File

@ -63,9 +63,10 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fontFamily="@font/gmarket_sans_medium" android:fontFamily="@font/gmarket_sans_medium"
android:gravity="center" android:gravity="center"
android:text="지금 예약중인 라이브가 없습니다.\n채널을 팔로잉 하고 라이브 알림을 받아 보세요." android:text="지금 예약중인 라이브가 없습니다.\n다른 날짜의 라이브를 예약하고 참여해 보세요."
android:textColor="@color/color_bbbbbb" android:textColor="@color/color_bbbbbb"
android:textSize="10.7sp" android:textSize="13sp"
android:lineSpacingExtra="8dp"
tools:ignore="SmallSp" /> tools:ignore="SmallSp" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

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