Compare commits

...

59 Commits

Author SHA1 Message Date
klaus e52075a692 라이브
- 팔로우/팔로잉 버튼 변경
2024-03-28 02:01:24 +09:00
klaus a29c50eae3 콘텐츠 상세
- 한정판이 매진된 경우 Player 가운데 Sold Out 표시
2024-03-28 01:35:21 +09:00
klaus 8a2a497fcf 콘텐츠 상세
- 한정판이 매진된 경우 구매버튼 비활성화 및 '해당 콘텐츠가 매진되었습니다.' 문구 표시
2024-03-27 18:59:28 +09:00
klaus 6786988e63 콘텐츠 상세
- 한정판이 매진된 경우 구매버튼 비활성화 및 '해당 콘텐츠가 매진되었습니다.' 문구 표시
2024-03-27 16:59:29 +09:00
klaus 2dd5e2d96c 콘텐츠 상세
- 한정판인 경우 대여/소장 선택 다이얼로그를 생략하고 바로 구매확인 다이얼로그가 나오도록 수정
2024-03-27 16:40:15 +09:00
klaus 03320d9cec 콘텐츠 상세
- 한정판 UI 추가
2024-03-27 00:04:28 +09:00
klaus 2101cdeb86 인 앱 결제 추가 2024-03-25 17:40:17 +09:00
klaus f0a8ca5823 Toast show 로직 수정 2024-03-23 06:05:08 +09:00
klaus daed389264 인 앱 결제 로직 수정
versionName 1.8.16, versionCode 41
2024-03-23 05:37:25 +09:00
klaus 6a558ad25c versionName 1.8.7, versionCode 32 2024-03-22 12:07:56 +09:00
klaus 6e3a4e1125 캔 충전페이지
- 인 앱 결제 페이지 추가
2024-03-21 04:14:52 +09:00
klaus 79cb4b995a 캔 충전페이지
- 인 앱 결제 추가를 위해 단일 액티비티에서 탭 구성으로 변경
2024-03-20 01:24:39 +09:00
klaus 84b9dd0841 versionCode 27 versionName "1.8.2" 2024-03-15 18:05:52 +09:00
klaus 4004dcd99e 라이브 메인 지금 라이브 중
- 이미지 크기 가로 116 -> 128, 세로 164 -> 179
2024-03-15 00:49:11 +09:00
klaus ebbe7e8917 라이브 메인 지금 라이브 중, 라이브 전체 보기
- 이미지 크기 수정, 잠금 아이콘 사이즈 40 -> 60
2024-03-14 23:51:37 +09:00
klaus 196d5c6cfd 라이브 메인 - 지금 라이브 중
- 입장 가능한 잔여 인원 표시 제거
2024-03-14 23:20:00 +09:00
klaus f4626a7cd5 라이브 - 호스트는 리스너로 변경 버튼이 보이지 않도록 수정 2024-03-14 22:57:04 +09:00
klaus 4ed97d4600 콘텐츠 리스트, 라이브 중 전체보기
- 그리드 아이템 사이 간격 수정
2024-03-14 19:13:21 +09:00
klaus c2cf05ef64 콘텐츠 리스트, 라이브 중 전체보기
- 그리드 아이템 사이 간격 수정
2024-03-14 18:46:22 +09:00
klaus 0e131f6661 콘텐츠 리스트, 라이브 중 전체보기
- 그리드 아이템 사이 간격 수정
2024-03-14 18:27:25 +09:00
klaus d091c47a0c 콘텐츠 리스트, 라이브 중 전체보기
- 그리드 아이템 사이 간격 수정
2024-03-14 18:01:45 +09:00
klaus 6bd5d26882 지금 라이브 중 전체보기 - UI 표시 방식 수정
- 1줄에 1개 보이던 리스트 방식에서 1줄에 3개씩 표시하는 그리드 방식으로 변경
2024-03-14 17:37:05 +09:00
klaus e02ea116ff 지금 라이브 중 - 참여 가능 인원 표시 2024-03-14 17:22:37 +09:00
klaus 012d1d94d5 지금 라이브 중 - 라이브 아이템 유/무료 표시 방식 수정
- 무료 배경색 : #111111
- 유료 배경색 : #DD4500
- 유료 표시 - white 캔과 함께 100 과 같이 가격으로 변경
2024-03-14 16:50:57 +09:00
klaus 39e49b08d9 본인이 스피커일 때 리스너로 변경하는 버튼 추가 2024-03-14 14:48:43 +09:00
klaus 2a84d2dc41 시그니처 재생 길이 수정
- 3초에서 7초로 변경
2024-03-14 14:41:21 +09:00
klaus 66c9b38e04 versionCode 26 versionName "1.8.1" 2024-03-11 12:24:44 +09:00
klaus 8f35f7a573 라이브 정보 수정
- 메뉴 설정을 하지 않고 다른 라이브 정보만 수정 했을 경우 앱이 종료 되는 버그 수정
(selectedMenu가 초기화 되지 않아도 라이브 정보를 수정할 수 있도록 수정)
2024-03-11 12:16:49 +09:00
klaus c653563512 라이브 만들기
- 메뉴 등록 edittext 스크롤 적용
2024-03-08 23:04:54 +09:00
klaus a75821ed06 스플래시 화면 변경 2024-03-08 21:04:51 +09:00
klaus ca6416c697 라이브 정보 변경 다이얼로그
- 공지, 메뉴 입력 창 스크롤 적용
2024-03-08 04:55:49 +09:00
klaus f7b3caf320 스플래시 화면 변경 2024-03-08 04:49:34 +09:00
klaus 62d839b69b 시그니처 후원 이미지 표시
- gif만 표시되던 것 모든 이미지로 변경
2024-03-08 04:47:47 +09:00
klaus a9c3ea953d 시그니처 후원 이미지 표시
- 이미지 URL을 배열에 저장 후 순서대로 표시하도록 수정
2024-03-08 02:46:25 +09:00
klaus 5be9720fcc 시그니처 후원 이미지 표시
- 하단 마진 20 -> 65로 변경
2024-03-08 01:37:55 +09:00
klaus ec096b5831 후원
- 시그니처 후원 적용
2024-03-08 00:51:43 +09:00
klaus 51c5e5f32c 에러가 아닌데 에러로 표시하는 부분 warning으로 변경 2024-03-07 06:33:10 +09:00
klaus b6d7e0b0e9 불필요한 로그 제거 2024-03-07 06:32:22 +09:00
klaus 49209c4c4a 라이브 정보 수정
- 메뉴판 수정 기능 추가
2024-03-07 06:31:25 +09:00
klaus 6b466ac7d7 라이브 메인 지금 라이브 중
- 가격 유/무료 뱃지 배경색 3bb9f1(버튼색)으로 변경
2024-03-07 02:36:05 +09:00
klaus d6182f9e03 라이브 메인 추천 채널
- 라이브 중 표시 컬러 9970ff -> 3bb9f1로 변경
2024-03-07 02:33:28 +09:00
klaus 2e24a298ff 라이브 만들기
- 메뉴판 설정 추가
2024-03-07 02:29:38 +09:00
klaus d7d43bc7be 라이브
- 메뉴판 UI 추가
2024-03-06 17:02:37 +09:00
klaus 2d0c4ea738 룰렛 설정 개수에 따라 룰렛 프리셋 버튼 활성화/비활성화 2024-02-28 03:56:51 +09:00
klaus af4e802259 룰렛 설정 문구 수정 2024-02-28 03:39:42 +09:00
klaus ad9e97161c 커뮤니티 댓글, 콘텐츠 댓글, 팬토크
- 여러줄 인 경우 줄간격 수정
2024-02-28 02:25:44 +09:00
klaus 1712f509dc gaid 업데이트 추가 2024-02-26 21:56:10 +09:00
klaus 78dd3b2785 커뮤니티 댓글, 콘텐츠 댓글, 팬토크
- 글자 크기, 색상 수정
2024-02-24 23:30:56 +09:00
klaus f7f4638526 룰렛 프리셋 설정
- 변동 사항이 있을 경우에만 업데이트 하도록 수정
2024-02-23 18:39:42 +09:00
klaus 66690a6f89 룰렛 프리셋 설정 성공 메시지
- 현재 선택된 룰렛 프리셋의 활성화/비활성화에 따라 성공 메시지가 수정되도록 수정
2024-02-23 17:57:49 +09:00
klaus 4e56eb9bde 룰렛 프리셋 적용 2024-02-23 14:07:52 +09:00
klaus e3349e41a1 versionCode 23 versionName "1.6.1" 2024-02-16 14:10:34 +09:00
klaus 239ccb9018 튜토리얼 이미지 변경 2024-02-16 14:09:49 +09:00
klaus 6ff17e1ba6 콘텐츠 그리드 리스트
- 아이템 상하 간격 16으로 변경
2024-02-15 00:58:52 +09:00
klaus 1c88a90024 튜토리얼 이미지 순서 변경 2024-02-15 00:55:41 +09:00
klaus dd54e5b97a 튜토리얼 수정 2024-02-14 23:59:28 +09:00
klaus f4180eec14 콘텐츠 메인 숏플, 라이브 다시 듣기 버튼 수정
- 부가 설명 제거
- 버튼 배경 이미지로 변경
- 글자 크기 14.7 -> 16.7로 변경
- 아이콘 큰 사이즈로 변경
2024-02-14 23:39:53 +09:00
klaus b81576bdaf 스플래시 화면 이미지 변경 2024-02-14 23:30:40 +09:00
klaus bf43926d7d 라이브 방
- 후원 시 '20캔을' 색을 바뀌던 것을 '20캔'까지만 색이 바뀌도록 수정
2024-02-14 23:12:15 +09:00
121 changed files with 3086 additions and 690 deletions

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 22 versionCode 46
versionName "1.6.0" versionName "1.8.21"
} }
buildTypes { buildTypes {
@ -149,4 +149,7 @@ dependencies {
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0' annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
implementation "com.michalsvec:single-row-calednar:1.0.0" implementation "com.michalsvec:single-row-calednar:1.0.0"
// google in-app-purchase
implementation "com.android.billingclient:billing-ktx:6.2.0"
} }

View File

@ -2,6 +2,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="com.android.vending.BILLING" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
@ -84,7 +85,9 @@
<activity android:name=".settings.terms.TermsActivity" /> <activity android:name=".settings.terms.TermsActivity" />
<activity android:name=".user.find_password.FindPasswordActivity" /> <activity android:name=".user.find_password.FindPasswordActivity" />
<activity android:name=".mypage.can.status.CanStatusActivity" /> <activity android:name=".mypage.can.status.CanStatusActivity" />
<activity android:name=".mypage.can.charge.CanChargeActivity" /> <activity
android:name=".mypage.can.charge.CanChargeActivity"
android:configChanges="orientation|screenSize|keyboardHidden" />
<activity android:name=".mypage.can.payment.CanPaymentActivity" /> <activity android:name=".mypage.can.payment.CanPaymentActivity" />
<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" />

View File

@ -12,6 +12,7 @@ import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.audio_content.main.new_content.AudioContentMainNewContentThemeAdapter import kr.co.vividnext.sodalive.audio_content.main.new_content.AudioContentMainNewContentThemeAdapter
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.GridSpacingItemDecoration
import kr.co.vividnext.sodalive.common.LoadingDialog import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentNewAllBinding import kr.co.vividnext.sodalive.databinding.ActivityAudioContentNewAllBinding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
@ -89,8 +90,10 @@ class AudioContentNewAllActivity : BaseActivity<ActivityAudioContentNewAllBindin
} }
private fun setupNewContent() { private fun setupNewContent() {
val spanCount = 3
val spacing = 40
newContentAdapter = AudioContentNewAllAdapter( newContentAdapter = AudioContentNewAllAdapter(
itemWidth = (screenWidth - 42f.dpToPx().toInt()) / 3, itemWidth = (screenWidth - (spacing * (spanCount + 1))) / 3,
onClickItem = { onClickItem = {
startActivity( startActivity(
Intent(this, AudioContentDetailActivity::class.java).apply { Intent(this, AudioContentDetailActivity::class.java).apply {
@ -107,23 +110,8 @@ class AudioContentNewAllActivity : BaseActivity<ActivityAudioContentNewAllBindin
} }
) )
binding.rvContent.layoutManager = GridLayoutManager(this, 3) binding.rvContent.layoutManager = GridLayoutManager(this, spanCount)
binding.rvContent.addItemDecoration(GridSpacingItemDecoration(spanCount, spacing, true))
binding.rvContent.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
outRect.top = 8f.dpToPx().toInt()
outRect.bottom = 8f.dpToPx().toInt()
outRect.left = 4f.dpToPx().toInt()
outRect.right = 4f.dpToPx().toInt()
}
})
binding.rvContent.addOnScrollListener(object : RecyclerView.OnScrollListener() { binding.rvContent.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {

View File

@ -1,6 +1,9 @@
package kr.co.vividnext.sodalive.audio_content.all package kr.co.vividnext.sodalive.audio_content.all
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -8,8 +11,12 @@ import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.load import coil.load
import coil.transform.CircleCropTransformation import coil.transform.CircleCropTransformation
import coil.transform.RoundedCornersTransformation import com.bumptech.glide.Glide
import com.orhanobut.logger.Logger import com.bumptech.glide.load.resource.bitmap.CenterCrop
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
import kr.co.vividnext.sodalive.databinding.ItemAudioContentNewAllBinding import kr.co.vividnext.sodalive.databinding.ItemAudioContentNewAllBinding
@ -23,24 +30,27 @@ class AudioContentNewAllAdapter(
) : RecyclerView.Adapter<AudioContentNewAllAdapter.ViewHolder>() { ) : RecyclerView.Adapter<AudioContentNewAllAdapter.ViewHolder>() {
inner class ViewHolder( inner class ViewHolder(
private val context: Context,
private val binding: ItemAudioContentNewAllBinding, private val binding: ItemAudioContentNewAllBinding,
private val onClickItem: (Long) -> Unit, private val onClickItem: (Long) -> Unit,
private val onClickCreator: (Long) -> Unit private val onClickCreator: (Long) -> Unit
) : RecyclerView.ViewHolder(binding.root) { ) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetAudioContentMainItem) { fun bind(item: GetAudioContentMainItem) {
Logger.e("item: $item") Glide
binding.ivAudioContentCoverImage.load(item.coverImageUrl) { .with(context)
crossfade(true) .load(item.coverImageUrl)
placeholder(R.drawable.ic_place_holder) .apply(
transformations(RoundedCornersTransformation(2.7f.dpToPx())) RequestOptions().transform(
CenterCrop(),
RoundedCorners(8)
)
)
.into(binding.ivAudioContentCoverImage)
val layoutParams = binding.ivAudioContentCoverImage val layoutParams = binding.ivAudioContentCoverImage.layoutParams as ConstraintLayout.LayoutParams
.layoutParams as ConstraintLayout.LayoutParams layoutParams.width = itemWidth
layoutParams.height = itemWidth
layoutParams.width = itemWidth binding.ivAudioContentCoverImage.layoutParams = layoutParams
layoutParams.height = itemWidth
binding.ivAudioContentCoverImage.layoutParams = layoutParams
}
binding.ivAudioContentCreator.load(item.creatorProfileImageUrl) { binding.ivAudioContentCreator.load(item.creatorProfileImageUrl) {
crossfade(true) crossfade(true)
@ -72,6 +82,7 @@ class AudioContentNewAllAdapter(
parent: ViewGroup, parent: ViewGroup,
viewType: Int viewType: Int
) = ViewHolder( ) = ViewHolder(
parent.context,
ItemAudioContentNewAllBinding.inflate( ItemAudioContentNewAllBinding.inflate(
LayoutInflater.from(parent.context), LayoutInflater.from(parent.context),
parent, parent,

View File

@ -16,6 +16,7 @@ import kr.co.vividnext.sodalive.audio_content.all.AudioContentNewAllAdapter
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
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.GridSpacingItemDecoration
import kr.co.vividnext.sodalive.common.LoadingDialog import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentAllByThemeBinding import kr.co.vividnext.sodalive.databinding.ActivityAudioContentAllByThemeBinding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
@ -55,8 +56,10 @@ class AudioContentAllByThemeActivity : BaseActivity<ActivityAudioContentAllByThe
loadingDialog = LoadingDialog(this, layoutInflater) loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.setOnClickListener { finish() } binding.toolbar.tvBack.setOnClickListener { finish() }
val spanCount = 3
val spacing = 40
adapter = AudioContentNewAllAdapter( adapter = AudioContentNewAllAdapter(
itemWidth = (screenWidth - 42f.dpToPx().toInt()) / 3, itemWidth = (screenWidth - (spacing * (spanCount + 1))) / 3,
onClickItem = { onClickItem = {
startActivity( startActivity(
Intent(this, AudioContentDetailActivity::class.java).apply { Intent(this, AudioContentDetailActivity::class.java).apply {
@ -73,23 +76,8 @@ class AudioContentAllByThemeActivity : BaseActivity<ActivityAudioContentAllByThe
} }
) )
binding.rvContentAll.layoutManager = GridLayoutManager(this, 3) binding.rvContentAll.layoutManager = GridLayoutManager(this, spanCount)
binding.rvContentAll.addItemDecoration(GridSpacingItemDecoration(spanCount, spacing, true))
binding.rvContentAll.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
outRect.top = 8f.dpToPx().toInt()
outRect.bottom = 8f.dpToPx().toInt()
outRect.left = 4f.dpToPx().toInt()
outRect.right = 4f.dpToPx().toInt()
}
})
binding.rvContentAll.addOnScrollListener(object : RecyclerView.OnScrollListener() { binding.rvContentAll.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {

View File

@ -17,6 +17,7 @@ import kr.co.vividnext.sodalive.audio_content.all.AudioContentNewAllAdapter
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
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.GridSpacingItemDecoration
import kr.co.vividnext.sodalive.common.LoadingDialog import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentCurationBinding import kr.co.vividnext.sodalive.databinding.ActivityAudioContentCurationBinding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
@ -54,8 +55,10 @@ class AudioContentCurationActivity : BaseActivity<ActivityAudioContentCurationBi
binding.toolbar.tvBack.text = title binding.toolbar.tvBack.text = title
binding.toolbar.tvBack.setOnClickListener { finish() } binding.toolbar.tvBack.setOnClickListener { finish() }
val spanCount = 3
val spacing = 40
adapter = AudioContentNewAllAdapter( adapter = AudioContentNewAllAdapter(
itemWidth = (screenWidth - 42f.dpToPx().toInt()) / 3, itemWidth = (screenWidth - (spacing * (spanCount + 1))) / 3,
onClickItem = { onClickItem = {
startActivity( startActivity(
Intent(this, AudioContentDetailActivity::class.java).apply { Intent(this, AudioContentDetailActivity::class.java).apply {
@ -72,23 +75,8 @@ class AudioContentCurationActivity : BaseActivity<ActivityAudioContentCurationBi
} }
) )
binding.rvCuration.layoutManager = GridLayoutManager(this, 3) binding.rvCuration.layoutManager = GridLayoutManager(this, spanCount)
binding.rvCuration.addItemDecoration(GridSpacingItemDecoration(spanCount, spacing, true))
binding.rvCuration.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
outRect.top = 8f.dpToPx().toInt()
outRect.bottom = 8f.dpToPx().toInt()
outRect.left = 4f.dpToPx().toInt()
outRect.right = 4f.dpToPx().toInt()
}
})
binding.rvCuration.addOnScrollListener(object : RecyclerView.OnScrollListener() { binding.rvCuration.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {

View File

@ -238,10 +238,16 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
} }
}) })
val layoutParams = binding.ivCover.layoutParams as RelativeLayout.LayoutParams val ivCoverLp = binding.ivCover.layoutParams as RelativeLayout.LayoutParams
layoutParams.width = (screenWidth - 13.3f.dpToPx()).toInt() ivCoverLp.width = (screenWidth - 13.3f.dpToPx()).toInt()
layoutParams.height = (screenWidth - 13.3f.dpToPx()).toInt() ivCoverLp.height = (screenWidth - 13.3f.dpToPx()).toInt()
binding.ivCover.layoutParams = layoutParams binding.ivCover.layoutParams = ivCoverLp
val flSoldOutLp = binding.flSoldOut.layoutParams as RelativeLayout.LayoutParams
flSoldOutLp.width = (screenWidth - 13.3f.dpToPx()).toInt()
flSoldOutLp.height = (screenWidth - 13.3f.dpToPx()).toInt()
binding.flSoldOut.layoutParams = flSoldOutLp
binding.ivPlayLoop.setOnClickListener { viewModel.togglePlayLoop() } binding.ivPlayLoop.setOnClickListener { viewModel.togglePlayLoop() }
binding.llDonation.setOnClickListener { binding.llDonation.setOnClickListener {
val dialog = LiveRoomDonationDialog( val dialog = LiveRoomDonationDialog(
@ -481,6 +487,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
if (response.releaseDate != null) { if (response.releaseDate != null) {
binding.llPurchase.visibility = View.VISIBLE binding.llPurchase.visibility = View.VISIBLE
binding.llPurchasePrice.visibility = View.GONE binding.llPurchasePrice.visibility = View.GONE
binding.tvPurchaseSoldOut.visibility = View.GONE
binding.tvReleaseDate.visibility = View.VISIBLE binding.tvReleaseDate.visibility = View.VISIBLE
binding.llPurchase.background = ContextCompat.getDrawable( binding.llPurchase.background = ContextCompat.getDrawable(
applicationContext, applicationContext,
@ -494,26 +501,50 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
response.orderType == null && response.orderType == null &&
response.creator.creatorId != SharedPreferenceManager.userId response.creator.creatorId != SharedPreferenceManager.userId
) { ) {
binding.tvReleaseDate.visibility = View.GONE if (
binding.llPurchase.visibility = View.VISIBLE response.totalContentCount != null && response.remainingContentCount != null &&
binding.llPurchasePrice.visibility = View.VISIBLE response.remainingContentCount <= 0
binding.tvPrice.text = response.price.toString() ) {
binding.llPurchase.background = ContextCompat.getDrawable( binding.llPurchase.visibility = View.GONE
applicationContext, binding.tvPurchaseSoldOut.visibility = View.VISIBLE
R.drawable.bg_round_corner_5_3_3bb9f1
)
binding.tvStrPurchaseOrRental.text = if (response.isOnlyRental) {
" 대여하기"
} else { } else {
" 구매하기" binding.tvPurchaseSoldOut.visibility = View.GONE
} binding.tvReleaseDate.visibility = View.GONE
binding.llPurchase.visibility = View.VISIBLE
binding.llPurchasePrice.visibility = View.VISIBLE
binding.tvPrice.text = response.price.toString()
binding.llPurchase.background = ContextCompat.getDrawable(
applicationContext,
R.drawable.bg_round_corner_5_3_3bb9f1
)
binding.llPurchase.setOnClickListener { binding.tvStrPurchaseOrRental.text = if (response.isOnlyRental) {
showOrderDialog(audioContent = response, isOnlyRental = response.isOnlyRental) " 대여하기"
} else {
" 구매하기"
}
binding.llPurchase.setOnClickListener {
if (
response.totalContentCount != null &&
response.remainingContentCount != null
) {
showOrderConfirmDialog(
audioContent = response,
isOnlyRental = false,
OrderType.KEEP
)
} else {
showOrderDialog(
audioContent = response,
isOnlyRental = response.isOnlyRental
)
}
}
} }
} else { } else {
binding.llPurchase.visibility = View.GONE binding.llPurchase.visibility = View.GONE
binding.tvPurchaseSoldOut.visibility = View.GONE
} }
} }
@ -527,6 +558,8 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
.apply(RequestOptions().override((screenWidth - 13.3f.dpToPx()).toInt())) .apply(RequestOptions().override((screenWidth - 13.3f.dpToPx()).toInt()))
.into(binding.ivCover) .into(binding.ivCover)
binding.flSoldOut.visibility = View.GONE
binding.tvSoldOutBig.visibility = View.GONE
binding.ivPlayOrPause.visibility = View.GONE binding.ivPlayOrPause.visibility = View.GONE
binding.tvPreviewNo.visibility = View.GONE binding.tvPreviewNo.visibility = View.GONE
binding.tvTotalDuration.text = " / ${response.duration}" binding.tvTotalDuration.text = " / ${response.duration}"
@ -536,9 +569,16 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
response.price > 0 response.price > 0
if ( if (
!response.existOrdered &&
response.totalContentCount != null && response.remainingContentCount != null &&
response.remainingContentCount <= 0
) {
binding.flSoldOut.visibility = View.VISIBLE
binding.tvSoldOutBig.visibility = View.VISIBLE
} else if (
response.releaseDate == null && response.releaseDate == null &&
!isAlertPreview || !isAlertPreview ||
(isAlertPreview && response.isActivePreview) (response.isActivePreview && response.contentUrl.isNotBlank())
) { ) {
binding.ivPlayOrPause.visibility = View.VISIBLE binding.ivPlayOrPause.visibility = View.VISIBLE
binding.ivPlayOrPause.setOnClickListener { binding.ivPlayOrPause.setOnClickListener {
@ -572,7 +612,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
R.drawable.btn_audio_content_preview_play R.drawable.btn_audio_content_preview_play
} }
) )
} else { } else if (response.releaseDate == null) {
binding.tvPreviewNo.visibility = View.VISIBLE binding.tvPreviewNo.visibility = View.VISIBLE
} }
} }
@ -657,6 +697,33 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
startActivity(shareIntent) startActivity(shareIntent)
} }
} }
if (response.totalContentCount != null && response.remainingContentCount != null) {
binding.rlLimitedEdition.visibility = View.VISIBLE
if (response.existOrdered) {
binding.tvRemaining.visibility = View.GONE
binding.tvTotalCount.visibility = View.VISIBLE
binding.tvRemainingCount.visibility = View.VISIBLE
binding.tvRemainingCount.text = "${response.orderSequence}"
binding.tvTotalCount.text = " / ${response.totalContentCount}"
} else if (response.remainingContentCount <= 0) {
binding.tvRemainingCount.visibility = View.GONE
binding.tvTotalCount.visibility = View.GONE
binding.tvRemaining.visibility = View.GONE
binding.tvSoldOutSmall.visibility = View.VISIBLE
} else {
binding.tvRemainingCount.visibility = View.VISIBLE
binding.tvRemaining.visibility = View.VISIBLE
binding.tvTotalCount.visibility = View.GONE
binding.tvRemainingCount.text = "${response.remainingContentCount}"
}
} else {
binding.rlLimitedEdition.visibility = View.GONE
}
} }
private fun setupMosaicArea(isMosaic: Boolean) { private fun setupMosaicArea(isMosaic: Boolean) {
@ -702,7 +769,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
binding.ivFollow.visibility = View.VISIBLE binding.ivFollow.visibility = View.VISIBLE
if (creator.isFollowing) { if (creator.isFollowing) {
binding.ivFollow.setImageResource(R.drawable.btn_notification_selected) binding.ivFollow.setImageResource(R.drawable.btn_following_big)
binding.ivFollow.setOnClickListener { binding.ivFollow.setOnClickListener {
viewModel.unRegisterNotification( viewModel.unRegisterNotification(
contentId = audioContentId, contentId = audioContentId,
@ -710,7 +777,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
) )
} }
} else { } else {
binding.ivFollow.setImageResource(R.drawable.btn_notification) binding.ivFollow.setImageResource(R.drawable.btn_follow_big)
binding.ivFollow.setOnClickListener { binding.ivFollow.setOnClickListener {
viewModel.registerNotification( viewModel.registerNotification(
contentId = audioContentId, contentId = audioContentId,

View File

@ -15,6 +15,9 @@ data class GetAudioContentDetailResponse(
@SerializedName("price") val price: Int, @SerializedName("price") val price: Int,
@SerializedName("duration") val duration: String, @SerializedName("duration") val duration: String,
@SerializedName("releaseDate") val releaseDate: String?, @SerializedName("releaseDate") val releaseDate: String?,
@SerializedName("totalContentCount") val totalContentCount: Int?,
@SerializedName("remainingContentCount") val remainingContentCount: Int?,
@SerializedName("orderSequence") val orderSequence: Int?,
@SerializedName("isActivePreview") val isActivePreview: Boolean, @SerializedName("isActivePreview") val isActivePreview: Boolean,
@SerializedName("isAdult") val isAdult: Boolean, @SerializedName("isAdult") val isAdult: Boolean,
@SerializedName("isMosaic") val isMosaic: Boolean, @SerializedName("isMosaic") val isMosaic: Boolean,

View File

@ -0,0 +1,37 @@
package kr.co.vividnext.sodalive.common
import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ItemDecoration
class GridSpacingItemDecoration(
private val spanCount: Int,
private val spacing: Int,
private val includeEdge: Boolean
) : ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
val position = parent.getChildAdapterPosition(view) // Item position
val column = position % spanCount // Current column
if (includeEdge) {
outRect.left = spacing - column * spacing / spanCount
outRect.right = (column + 1) * spacing / spanCount
if (position < spanCount) { // Top edge
outRect.top = spacing
}
outRect.bottom = spacing // Item bottom
} else {
outRect.left = column * spacing / spanCount
outRect.right = spacing - (column + 1) * spacing / spanCount
if (position >= spanCount) {
outRect.top = spacing // Item top
}
}
}
}

View File

@ -53,6 +53,7 @@ import kr.co.vividnext.sodalive.live.room.LiveRoomViewModel
import kr.co.vividnext.sodalive.live.room.create.LiveRoomCreateViewModel import kr.co.vividnext.sodalive.live.room.create.LiveRoomCreateViewModel
import kr.co.vividnext.sodalive.live.room.detail.LiveRoomDetailViewModel import kr.co.vividnext.sodalive.live.room.detail.LiveRoomDetailViewModel
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.menu.MenuApi
import kr.co.vividnext.sodalive.live.room.tag.LiveTagRepository import kr.co.vividnext.sodalive.live.room.tag.LiveTagRepository
import kr.co.vividnext.sodalive.live.room.tag.LiveTagViewModel import kr.co.vividnext.sodalive.live.room.tag.LiveTagViewModel
import kr.co.vividnext.sodalive.live.room.update.LiveRoomEditViewModel import kr.co.vividnext.sodalive.live.room.update.LiveRoomEditViewModel
@ -73,7 +74,8 @@ import kr.co.vividnext.sodalive.mypage.auth.AuthApi
import kr.co.vividnext.sodalive.mypage.auth.AuthRepository import kr.co.vividnext.sodalive.mypage.auth.AuthRepository
import kr.co.vividnext.sodalive.mypage.can.CanApi import kr.co.vividnext.sodalive.mypage.can.CanApi
import kr.co.vividnext.sodalive.mypage.can.CanRepository import kr.co.vividnext.sodalive.mypage.can.CanRepository
import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeViewModel 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.coupon.CanCouponViewModel import kr.co.vividnext.sodalive.mypage.can.coupon.CanCouponViewModel
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.status.CanStatusViewModel import kr.co.vividnext.sodalive.mypage.can.status.CanStatusViewModel
@ -151,6 +153,7 @@ 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(), 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(), 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) }
@ -176,7 +179,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
viewModel { LiveViewModel(get(), get(), get(), get()) } viewModel { LiveViewModel(get(), get(), get(), get()) }
viewModel { MyPageViewModel(get(), get()) } viewModel { MyPageViewModel(get(), get()) }
viewModel { CanStatusViewModel(get()) } viewModel { CanStatusViewModel(get()) }
viewModel { CanChargeViewModel(get()) } viewModel { CanChargePgViewModel(get()) }
viewModel { CanPaymentViewModel(get()) } viewModel { CanPaymentViewModel(get()) }
viewModel { LiveRoomDetailViewModel(get()) } viewModel { LiveRoomDetailViewModel(get()) }
viewModel { LiveRoomCreateViewModel(get()) } viewModel { LiveRoomCreateViewModel(get()) }
@ -229,12 +232,13 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
viewModel { CreatorCommunityWriteViewModel(get()) } viewModel { CreatorCommunityWriteViewModel(get()) }
viewModel { CreatorCommunityModifyViewModel(get()) } viewModel { CreatorCommunityModifyViewModel(get()) }
viewModel { CanCouponViewModel(get()) } viewModel { CanCouponViewModel(get()) }
viewModel { CanChargeIapViewModel(get()) }
} }
private val repositoryModule = module { private val repositoryModule = module {
factory { UserRepository(get()) } factory { UserRepository(get()) }
factory { TermsRepository(get()) } factory { TermsRepository(get()) }
factory { LiveRepository(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()) }

View File

@ -615,12 +615,12 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
} }
if (creator.isNotification) { if (creator.isNotification) {
layoutUserProfile.ivNotification.setImageResource(R.drawable.btn_notification_selected) layoutUserProfile.ivNotification.setImageResource(R.drawable.btn_following_big)
layoutUserProfile.ivNotification.setOnClickListener { layoutUserProfile.ivNotification.setOnClickListener {
viewModel.unFollow(creator.creatorId) viewModel.unFollow(creator.creatorId)
} }
} else { } else {
layoutUserProfile.ivNotification.setImageResource(R.drawable.btn_notification) layoutUserProfile.ivNotification.setImageResource(R.drawable.btn_follow_big)
layoutUserProfile.ivNotification.setOnClickListener { layoutUserProfile.ivNotification.setOnClickListener {
viewModel.follow(creator.creatorId) viewModel.follow(creator.creatorId)
} }

View File

@ -31,13 +31,13 @@ class UserFollowerListAdapter(
if (item.isFollow != null) { if (item.isFollow != null) {
binding.ivNotification.visibility = View.VISIBLE binding.ivNotification.visibility = View.VISIBLE
if (item.isFollow) { if (item.isFollow) {
binding.ivNotification.setImageResource(R.drawable.btn_notification_selected) binding.ivNotification.setImageResource(R.drawable.btn_following_big)
binding.ivNotification.setOnClickListener { binding.ivNotification.setOnClickListener {
onClickUnRegisterNotification(item.userId) onClickUnRegisterNotification(item.userId)
clear() clear()
} }
} else { } else {
binding.ivNotification.setImageResource(R.drawable.btn_notification) binding.ivNotification.setImageResource(R.drawable.btn_follow_big)
binding.ivNotification.setOnClickListener { binding.ivNotification.setOnClickListener {
onClickRegisterNotification(item.userId) onClickRegisterNotification(item.userId)
clear() clear()

View File

@ -32,7 +32,7 @@ class FollowingCreatorAdapter(
binding.ivNotification.visibility = View.VISIBLE binding.ivNotification.visibility = View.VISIBLE
if (item.isFollow) { if (item.isFollow) {
binding.ivNotification.setImageResource(R.drawable.btn_notification_selected) binding.ivNotification.setImageResource(R.drawable.btn_following_big)
binding.ivNotification.setOnClickListener { binding.ivNotification.setOnClickListener {
val index = items.indexOf(item) val index = items.indexOf(item)
val copyItem = item.copy(isFollow = false) val copyItem = item.copy(isFollow = false)
@ -42,7 +42,7 @@ class FollowingCreatorAdapter(
onClickUnRegisterNotification(item.creatorId) onClickUnRegisterNotification(item.creatorId)
} }
} else { } else {
binding.ivNotification.setImageResource(R.drawable.btn_notification) binding.ivNotification.setImageResource(R.drawable.btn_follow_big)
binding.ivNotification.setOnClickListener { binding.ivNotification.setOnClickListener {
val index = items.indexOf(item) val index = items.indexOf(item)
val copyItem = item.copy(isFollow = true) val copyItem = item.copy(isFollow = true)

View File

@ -160,7 +160,7 @@ interface LiveApi {
fun donation( fun donation(
@Body request: LiveRoomDonationRequest, @Body request: LiveRoomDonationRequest,
@Header("Authorization") authHeader: String @Header("Authorization") authHeader: String
): Single<ApiResponse<Any>> ): Single<ApiResponse<String>>
@POST("/live/room/donation/refund/{id}") @POST("/live/room/donation/refund/{id}")
fun refundDonation( fun refundDonation(

View File

@ -15,6 +15,7 @@ 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.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.user.CreatorFollowRequestRequest import kr.co.vividnext.sodalive.user.CreatorFollowRequestRequest
import kr.co.vividnext.sodalive.user.UserApi import kr.co.vividnext.sodalive.user.UserApi
import okhttp3.MultipartBody import okhttp3.MultipartBody
@ -23,7 +24,8 @@ import java.util.TimeZone
class LiveRepository( class LiveRepository(
private val api: LiveApi, private val api: LiveApi,
private val userApi: UserApi private val userApi: UserApi,
private val menuApi: MenuApi
) { ) {
fun roomList( fun roomList(
dateString: String? = null, dateString: String? = null,
@ -161,7 +163,7 @@ class LiveRepository(
can: Int, can: Int,
message: String, message: String,
token: String token: String
): Single<ApiResponse<Any>> { ): Single<ApiResponse<String>> {
return api.donation( return api.donation(
request = LiveRoomDonationRequest( request = LiveRoomDonationRequest(
roomId = roomId, roomId = roomId,
@ -230,4 +232,9 @@ class LiveRepository(
request: CancelLiveReservationRequest, request: CancelLiveReservationRequest,
token: String token: String
) = api.cancelReservation(request, authHeader = token) ) = api.cancelReservation(request, authHeader = token)
fun getAllMenu(creatorId: Long, token: String) = menuApi.getAllMenu(
creatorId = creatorId,
authHeader = token
)
} }

View File

@ -1,12 +1,21 @@
package kr.co.vividnext.sodalive.live.now package kr.co.vividnext.sodalive.live.now
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
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 androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.transform.CircleCropTransformation import coil.transform.CircleCropTransformation
import coil.transform.RoundedCornersTransformation import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.CenterCrop
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemLiveNowBinding import kr.co.vividnext.sodalive.databinding.ItemLiveNowBinding
import kr.co.vividnext.sodalive.extensions.dpToPx import kr.co.vividnext.sodalive.extensions.dpToPx
@ -20,15 +29,28 @@ class LiveNowAdapter(
var items = mutableListOf<GetRoomListResponse>() var items = mutableListOf<GetRoomListResponse>()
inner class ViewHolder( inner class ViewHolder(
private val context: Context,
private val binding: ItemLiveNowBinding private val binding: ItemLiveNowBinding
) : RecyclerView.ViewHolder(binding.root) { ) : RecyclerView.ViewHolder(binding.root) {
@SuppressLint("SetTextI18n")
fun bind(item: GetRoomListResponse) { fun bind(item: GetRoomListResponse) {
binding.ivCover.loadUrl(item.coverImageUrl) { Glide
crossfade(true) .with(context)
placeholder(R.drawable.ic_place_holder) .load(item.coverImageUrl)
transformations(RoundedCornersTransformation(4.7f.dpToPx())) .apply(
} RequestOptions().transform(
CenterCrop(),
RoundedCorners(8)
)
)
.into(binding.ivCover)
val layoutParams = binding.ivCover.layoutParams as ConstraintLayout.LayoutParams
layoutParams.width = 128f.dpToPx().toInt()
layoutParams.height = 179f.dpToPx().toInt()
binding.ivCover.layoutParams = layoutParams
binding.ivLock.visibility = if (item.isPrivateRoom) { binding.ivLock.visibility = if (item.isPrivateRoom) {
View.VISIBLE View.VISIBLE
} else { } else {
@ -36,11 +58,18 @@ class LiveNowAdapter(
} }
if (item.price > 0) { if (item.price > 0) {
binding.tvPrice.text = "유료" binding.tvPrice.text = "${item.price}"
binding.tvPrice.setBackgroundResource(R.drawable.bg_round_corner_10_881609) binding.tvPrice.setCompoundDrawablesWithIntrinsicBounds(
R.drawable.ic_can_white,
0,
0,
0
)
binding.tvPrice.setBackgroundResource(R.drawable.bg_round_corner_13_3_dd4500)
} else { } else {
binding.tvPrice.text = "무료" binding.tvPrice.text = "무료"
binding.tvPrice.setBackgroundResource(R.drawable.bg_round_corner_10_643bc8) binding.tvPrice.setCompoundDrawables(null, null, null, null)
binding.tvPrice.setBackgroundResource(R.drawable.bg_round_corner_13_3_111111)
} }
if (item.tags.isNotEmpty()) { if (item.tags.isNotEmpty()) {
@ -63,6 +92,7 @@ class LiveNowAdapter(
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
parent.context,
ItemLiveNowBinding.inflate( ItemLiveNowBinding.inflate(
LayoutInflater.from(parent.context), LayoutInflater.from(parent.context),
parent, parent,

View File

@ -2,19 +2,19 @@ package kr.co.vividnext.sodalive.live.now.all
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.graphics.Rect
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.Toast import android.widget.Toast
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.audio_content.AudioContentPlayService import kr.co.vividnext.sodalive.audio_content.AudioContentPlayService
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.GridSpacingItemDecoration
import kr.co.vividnext.sodalive.common.LoadingDialog import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ActivityLiveNowAllBinding import kr.co.vividnext.sodalive.databinding.ActivityLiveNowAllBinding
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.LiveViewModel import kr.co.vividnext.sodalive.live.LiveViewModel
import kr.co.vividnext.sodalive.live.room.LiveRoomActivity import kr.co.vividnext.sodalive.live.room.LiveRoomActivity
@ -46,8 +46,10 @@ class LiveNowAllActivity : BaseActivity<ActivityLiveNowAllBinding>(
binding.toolbar.tvBack.setOnClickListener { finish() } binding.toolbar.tvBack.setOnClickListener { finish() }
loadingDialog = LoadingDialog(this, layoutInflater) loadingDialog = LoadingDialog(this, layoutInflater)
val spanCount = 3
val spacing = 40
val recyclerView = binding.rvLive val recyclerView = binding.rvLive
adapter = LiveNowAllAdapter { adapter = LiveNowAllAdapter(itemWidth = (screenWidth - (spacing * (spanCount + 1))) / 3) {
val detailFragment = LiveRoomDetailFragment( val detailFragment = LiveRoomDetailFragment(
it.roomId, it.roomId,
onClickParticipant = { enterLiveRoom(it.roomId) }, onClickParticipant = { enterLiveRoom(it.roomId) },
@ -63,41 +65,8 @@ class LiveNowAllActivity : BaseActivity<ActivityLiveNowAllBinding>(
) )
} }
recyclerView.layoutManager = LinearLayoutManager( recyclerView.layoutManager = GridLayoutManager(this, spanCount)
applicationContext, recyclerView.addItemDecoration(GridSpacingItemDecoration(spanCount, spacing, false))
LinearLayoutManager.VERTICAL,
false
)
recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.top = 0.dpToPx().toInt()
outRect.bottom = 6.7f.dpToPx().toInt()
}
adapter.itemCount - 1 -> {
outRect.top = 6.7f.dpToPx().toInt()
outRect.bottom = 0.dpToPx().toInt()
}
else -> {
outRect.top = 6.7f.dpToPx().toInt()
outRect.bottom = 6.7f.dpToPx().toInt()
}
}
}
})
recyclerView.adapter = adapter
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
@ -113,6 +82,8 @@ class LiveNowAllActivity : BaseActivity<ActivityLiveNowAllBinding>(
} }
}) })
recyclerView.adapter = adapter
binding.swipeRefreshLayout.setOnRefreshListener { binding.swipeRefreshLayout.setOnRefreshListener {
adapter.clear() adapter.clear()
viewModel.page = 1 viewModel.page = 1

View File

@ -2,20 +2,29 @@ package kr.co.vividnext.sodalive.live.now.all
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
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 androidx.core.content.ContextCompat import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.load import coil.transform.CircleCropTransformation
import coil.transform.RoundedCornersTransformation import coil.transform.RoundedCornersTransformation
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.CenterCrop
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemLiveNowAllBinding import kr.co.vividnext.sodalive.databinding.ItemLiveNowAllBinding
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.loadUrl
import kr.co.vividnext.sodalive.live.GetRoomListResponse import kr.co.vividnext.sodalive.live.GetRoomListResponse
class LiveNowAllAdapter( class LiveNowAllAdapter(
private val itemWidth: Int,
private val onClick: (GetRoomListResponse) -> Unit private val onClick: (GetRoomListResponse) -> Unit
) : RecyclerView.Adapter<LiveNowAllAdapter.ViewHolder>() { ) : RecyclerView.Adapter<LiveNowAllAdapter.ViewHolder>() {
@ -27,48 +36,72 @@ class LiveNowAllAdapter(
) : RecyclerView.ViewHolder(binding.root) { ) : RecyclerView.ViewHolder(binding.root) {
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
fun bind(item: GetRoomListResponse) { fun bind(item: GetRoomListResponse) {
binding.ivCover.load(item.coverImageUrl) { Glide
crossfade(true) .with(context)
placeholder(R.drawable.bg_placeholder) .load(item.coverImageUrl)
transformations(RoundedCornersTransformation(4.7f.dpToPx())) .apply(
} RequestOptions().transform(
binding.tvNickname.text = item.creatorNickname CenterCrop(),
binding.tvTitle.text = item.title RoundedCorners(14)
)
)
.into(binding.ivCover)
val layoutParams = binding.ivCover
.layoutParams as ConstraintLayout.LayoutParams
layoutParams.width = itemWidth
layoutParams.height = itemWidth * 144 / 102
binding.ivLock.visibility = if (item.isPrivateRoom) { binding.ivLock.visibility = if (item.isPrivateRoom) {
View.VISIBLE View.VISIBLE
} else { } else {
View.GONE View.GONE
} }
if (item.numberOfPeople > item.numberOfParticipate) { if (item.price > 0) {
binding.tvAvailableParticipate.text = "참여가능" binding.tvPrice.text = "${item.price}"
binding.tvAvailableParticipate.setTextColor(
ContextCompat.getColor(
context,
R.color.color_3bb9f1
)
)
} else {
binding.tvAvailableParticipate.text = "Sold out"
binding.tvAvailableParticipate.setTextColor(
ContextCompat.getColor(
context,
R.color.color_ffd300
)
)
}
if (item.price < 1) {
binding.tvPrice.text = "무료"
binding.tvPrice.setCompoundDrawables(null, null, null, null)
} else {
binding.tvPrice.text = item.price.moneyFormat()
binding.tvPrice.setCompoundDrawablesWithIntrinsicBounds( binding.tvPrice.setCompoundDrawablesWithIntrinsicBounds(
R.drawable.ic_can_white,
0, 0,
0, 0,
R.drawable.ic_can,
0 0
) )
binding.tvPrice.setBackgroundResource(R.drawable.bg_round_corner_13_3_dd4500)
} else {
binding.tvPrice.text = "무료"
binding.tvPrice.setCompoundDrawables(null, null, null, null)
binding.tvPrice.setBackgroundResource(R.drawable.bg_round_corner_13_3_111111)
}
if (item.tags.isNotEmpty()) {
binding.tvTags.visibility = View.VISIBLE
binding.tvTags.text = item.tags.joinToString(" ") { "#$it" }
} else {
binding.tvTags.visibility = View.GONE
}
binding.tvTitle.text = item.title
binding.tvNickname.text = item.creatorNickname
binding.ivProfile.loadUrl(item.creatorProfileImage) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}
if (item.numberOfPeople - item.numberOfParticipate <= 2) {
binding.llRemainingParticipant.visibility = View.VISIBLE
if (item.numberOfPeople > item.numberOfParticipate) {
binding.tvRemainingParticipantNumber.visibility = View.VISIBLE
binding.tvRemainingParticipant.text = "잔여"
binding.tvRemainingParticipantNumber.text =
"${item.numberOfPeople - item.numberOfParticipate}"
} else {
binding.tvRemainingParticipantNumber.visibility = View.GONE
binding.tvRemainingParticipant.text = "Sold out"
binding.tvRemainingParticipantNumber.text = ""
}
} else {
binding.llRemainingParticipant.visibility = View.GONE
} }
binding.root.setOnClickListener { onClick(item) } binding.root.setOnClickListener { onClick(item) }

View File

@ -26,12 +26,11 @@ import android.widget.Toast
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.constraintlayout.widget.ConstraintLayout
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
import coil.load
import coil.transform.CircleCropTransformation import coil.transform.CircleCropTransformation
import com.bumptech.glide.Glide
import com.github.dhaval2404.imagepicker.ImagePicker import com.github.dhaval2404.imagepicker.ImagePicker
import com.google.gson.Gson import com.google.gson.Gson
import com.orhanobut.logger.Logger import com.orhanobut.logger.Logger
@ -115,6 +114,18 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
private var isNoChatting = false private var isNoChatting = false
private var remainingNoChattingTime = noChattingTime private var remainingNoChattingTime = noChattingTime
private val signatureImageUrlList = mutableListOf<String>()
private var signatureImageUrl = ""
set(value) {
field = value
if (field.isNotBlank()) {
showSignatureImage()
}
}
private var isShowSignatureImage = false
private val countDownTimer = object : CountDownTimer(remainingNoChattingTime * 1000, 1000) { private val countDownTimer = object : CountDownTimer(remainingNoChattingTime * 1000, 1000) {
override fun onTick(millisUntilFinished: Long) { override fun onTick(millisUntilFinished: Long) {
remainingNoChattingTime -= 1 remainingNoChattingTime -= 1
@ -454,6 +465,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
dialog.show(screenWidth) dialog.show(screenWidth)
} }
binding.tvNotification.setOnClickListener { viewModel.toggleShowNotice() } binding.tvNotification.setOnClickListener { viewModel.toggleShowNotice() }
binding.tvMenuPan.setOnClickListener { viewModel.toggleShowMenuPan() }
binding.tvBgSwitch.setOnClickListener { viewModel.toggleBackgroundImage() } binding.tvBgSwitch.setOnClickListener { viewModel.toggleBackgroundImage() }
binding.llDonation.setOnClickListener { binding.llDonation.setOnClickListener {
@ -696,37 +708,44 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
if (response.creatorId == SharedPreferenceManager.userId) { if (response.creatorId == SharedPreferenceManager.userId) {
binding.ivEdit.setOnClickListener { binding.ivEdit.setOnClickListener {
roomInfoEditDialog.setRoomInfo(response.title, response.notice) viewModel.getAllMenuPreset {
roomInfoEditDialog.setCoverImageUrl(response.coverImageUrl) roomInfoEditDialog.setRoomInfo(response.title, response.notice)
roomInfoEditDialog.setConfirmAction { newTitle, newContent, newCoverImageUri -> roomInfoEditDialog.setCoverImageUrl(response.coverImageUrl)
viewModel.editLiveRoomInfo( roomInfoEditDialog.setMenuPreset(it)
response.roomId, roomInfoEditDialog.setConfirmAction { newTitle, newContent, newCoverImageUri, isActivateMenu, menuId, menu ->
newTitle, viewModel.editLiveRoomInfo(
newContent, response.roomId,
newCoverImageUri, newTitle,
onSuccess = { newContent,
binding.tvTitle.text = newTitle newCoverImageUri,
setNoticeAndClickableUrl(binding.tvNotice, newContent) isActivateMenu,
menuId,
menu,
onSuccess = {
Toast.makeText(
applicationContext,
"라이브 정보가 수정되었습니다.",
Toast.LENGTH_LONG
).show()
if (newCoverImageUri != null) { agora.sendRawMessageToGroup(
binding.ivCover.load(newCoverImageUri) rawMessage = Gson().toJson(
LiveRoomChatRawMessage(
type = LiveRoomChatRawMessageType.EDIT_ROOM_INFO,
message = "",
can = 0,
donationMessage = ""
)
).toByteArray()
)
} }
)
}
agora.sendRawMessageToGroup( handler.post {
rawMessage = Gson().toJson( roomInfoEditDialog.show(screenWidth)
LiveRoomChatRawMessage( }
type = LiveRoomChatRawMessageType.EDIT_ROOM_INFO,
message = "",
can = 0,
donationMessage = ""
)
).toByteArray()
)
}
)
} }
roomInfoEditDialog.show(screenWidth)
} }
binding.ivEdit.visibility = View.VISIBLE binding.ivEdit.visibility = View.VISIBLE
@ -781,7 +800,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
if (response.creatorId != SharedPreferenceManager.userId) { if (response.creatorId != SharedPreferenceManager.userId) {
binding.ivCreatorFollow.visibility = View.VISIBLE binding.ivCreatorFollow.visibility = View.VISIBLE
if (response.isFollowing) { if (response.isFollowing) {
binding.ivCreatorFollow.setImageResource(R.drawable.btn_select_checked) binding.ivCreatorFollow.setImageResource(R.drawable.btn_following)
binding.ivCreatorFollow.setOnClickListener { binding.ivCreatorFollow.setOnClickListener {
viewModel.creatorUnFollow( viewModel.creatorUnFollow(
creatorId = response.creatorId, creatorId = response.creatorId,
@ -789,7 +808,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
) )
} }
} else { } else {
binding.ivCreatorFollow.setImageResource(R.drawable.btn_plus_round) binding.ivCreatorFollow.setImageResource(R.drawable.btn_follow)
binding.ivCreatorFollow.setOnClickListener { binding.ivCreatorFollow.setOnClickListener {
viewModel.creatorFollow( viewModel.creatorFollow(
creatorId = response.creatorId, creatorId = response.creatorId,
@ -807,6 +826,15 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
isActiveRoulette = response.isActiveRoulette isActiveRoulette = response.isActiveRoulette
) )
if (response.menuPan.isNotBlank()) {
binding.tvMenuPan.visibility = View.VISIBLE
binding.tvMenuPanDetail.text = response.menuPan
} else {
viewModel.toggleShowMenuPan(false)
binding.tvMenuPan.visibility = View.GONE
binding.tvMenuPanDetail.text = ""
}
if (agora.rtmChannelIsNull()) { if (agora.rtmChannelIsNull()) {
joinChannel(response) joinChannel(response)
} }
@ -840,6 +868,34 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
} }
} }
viewModel.isShowMenuPan.observe(this) {
if (it) {
binding.tvMenuPan.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_3bb9f1
)
)
binding.tvMenuPan.setBackgroundResource(
R.drawable.bg_round_corner_5_3_transparent_3bb9f1
)
binding.llMenuPan.visibility = View.VISIBLE
} else {
binding.tvMenuPan.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_bbbbbb
)
)
binding.tvMenuPan.setBackgroundResource(
R.drawable.bg_round_corner_5_3_transparent_bbbbbb
)
binding.llMenuPan.visibility = View.GONE
}
}
viewModel.totalDonationCan.observe(this) { viewModel.totalDonationCan.observe(this) {
binding.tvTotalCan.text = it.moneyFormat() binding.tvTotalCan.text = it.moneyFormat()
} }
@ -1026,6 +1082,8 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
agora.muteLocalAudioStream(false) agora.muteLocalAudioStream(false)
agora.setClientRole(io.agora.rtc2.Constants.CLIENT_ROLE_AUDIENCE) agora.setClientRole(io.agora.rtc2.Constants.CLIENT_ROLE_AUDIENCE)
handler.postDelayed({ handler.postDelayed({
binding.tvChangeListener.visibility = View.GONE
binding.tvChangeListener.setOnClickListener { }
binding.ivMicrophoneMute.setImageResource(R.drawable.ic_mic_on) binding.ivMicrophoneMute.setImageResource(R.drawable.ic_mic_on)
binding.flMicrophoneMute.visibility = View.GONE binding.flMicrophoneMute.visibility = View.GONE
binding.ivNotiMicrophoneMute.visibility = View.GONE binding.ivNotiMicrophoneMute.visibility = View.GONE
@ -1211,16 +1269,18 @@ 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"
val donationRawMessage = Gson().toJson(
LiveRoomChatRawMessage(
type = LiveRoomChatRawMessageType.DONATION,
message = rawMessage,
can = can,
donationMessage = message
)
)
viewModel.donation(roomId, can, message) { viewModel.donation(roomId, can, message) { signatureImage ->
val donationRawMessage = Gson().toJson(
LiveRoomChatRawMessage(
type = LiveRoomChatRawMessageType.DONATION,
message = rawMessage,
can = can,
signatureImageUrl = signatureImage,
donationMessage = message
)
)
agora.sendRawMessageToGroup( agora.sendRawMessageToGroup(
rawMessage = donationRawMessage.toByteArray(), rawMessage = donationRawMessage.toByteArray(),
onSuccess = { onSuccess = {
@ -1240,6 +1300,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
) )
invalidateChat() invalidateChat()
viewModel.addDonationCan(can) viewModel.addDonationCan(can)
addSignatureImage(signatureImage)
} }
}, },
onFailure = { onFailure = {
@ -1346,6 +1407,9 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
) )
invalidateChat() invalidateChat()
viewModel.addDonationCan(rawMessage.can) viewModel.addDonationCan(rawMessage.can)
addSignatureImage(
imageUrl = rawMessage.signatureImageUrl ?: ""
)
} }
} }
@ -1622,6 +1686,18 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
showDialog(content = "스피커가 되었어요!") showDialog(content = "스피커가 되었어요!")
setBroadcaster() setBroadcaster()
viewModel.getRoomInfo(roomId) viewModel.getRoomInfo(roomId)
binding.tvChangeListener.visibility = View.VISIBLE
binding.tvChangeListener.setOnClickListener {
handler.post {
viewModel.setListener(
roomId,
SharedPreferenceManager.userId
) {
setAudience()
viewModel.getRoomInfo(roomId)
}
}
}
} }
} }
} }
@ -1675,6 +1751,38 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
} }
} }
private fun addSignatureImage(imageUrl: String) {
if (imageUrl.isNotBlank()) {
if (!isShowSignatureImage) {
isShowSignatureImage = true
signatureImageUrl = imageUrl
} else {
signatureImageUrlList.add(imageUrl)
}
}
}
private fun showSignatureImage() {
if (signatureImageUrl.isNotBlank()) {
Glide
.with(this)
.load(signatureImageUrl)
.into(binding.ivSignature)
binding.ivSignature.visibility = View.VISIBLE
handler.postDelayed({
if (signatureImageUrlList.isNotEmpty()) {
signatureImageUrl = signatureImageUrlList.removeAt(0)
} else {
signatureImageUrl = ""
isShowSignatureImage = false
binding.ivSignature.setImageDrawable(null)
binding.ivSignature.visibility = View.GONE
}
}, 7000)
}
}
companion object { companion object {
private const val noChattingTime = 180L private const val noChattingTime = 180L
} }

View File

@ -18,6 +18,7 @@ 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.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.profile.GetLiveRoomUserProfileResponse import kr.co.vividnext.sodalive.live.room.profile.GetLiveRoomUserProfileResponse
import kr.co.vividnext.sodalive.live.room.update.EditLiveRoomInfoRequest import kr.co.vividnext.sodalive.live.room.update.EditLiveRoomInfoRequest
import kr.co.vividnext.sodalive.live.roulette.RouletteItem import kr.co.vividnext.sodalive.live.roulette.RouletteItem
@ -54,6 +55,10 @@ class LiveRoomViewModel(
val isShowNotice: LiveData<Boolean> val isShowNotice: LiveData<Boolean>
get() = _isShowNotice get() = _isShowNotice
private val _isShowMenuPan = MutableLiveData(false)
val isShowMenuPan: LiveData<Boolean>
get() = _isShowMenuPan
private val _totalDonationCan = MutableLiveData(0) private val _totalDonationCan = MutableLiveData(0)
val totalDonationCan: LiveData<Int> val totalDonationCan: LiveData<Int>
get() = _totalDonationCan get() = _totalDonationCan
@ -355,9 +360,20 @@ class LiveRoomViewModel(
} }
fun toggleShowNotice() { fun toggleShowNotice() {
_isShowMenuPan.value = false
_isShowNotice.value = !isShowNotice.value!! _isShowNotice.value = !isShowNotice.value!!
} }
fun toggleShowMenuPan(isShowMenuPan: Boolean? = null) {
_isShowNotice.value = false
if (isShowMenuPan != null) {
_isShowMenuPan.value = isShowMenuPan
} else {
_isShowMenuPan.value = !this.isShowMenuPan.value!!
}
}
fun toggleBackgroundImage() { fun toggleBackgroundImage() {
_isBgOn.value = !isBgOn.value!! _isBgOn.value = !isBgOn.value!!
} }
@ -367,6 +383,9 @@ class LiveRoomViewModel(
newTitle: String, newTitle: String,
newContent: String, newContent: String,
newCoverImageUri: Uri? = null, newCoverImageUri: Uri? = null,
isActivateMenu: Boolean?,
menuId: Long,
menu: String,
onSuccess: () -> Unit onSuccess: () -> Unit
) { ) {
val request = EditLiveRoomInfoRequest( val request = EditLiveRoomInfoRequest(
@ -382,17 +401,25 @@ class LiveRoomViewModel(
}, },
numberOfPeople = null, numberOfPeople = null,
beginDateTimeString = null, beginDateTimeString = null,
timezone = null timezone = null,
menuPanId = if (isActivateMenu == true) menuId else 0,
menuPan = if (isActivateMenu == true) menu else "",
isActiveMenuPan = isActivateMenu
) )
val requestJson = if (request.title != null || request.notice != null) { val requestJson = if (
request.title != null ||
request.notice != null ||
request.isActiveMenuPan != null ||
request.menuPan.isNotBlank()
) {
Gson().toJson(request) Gson().toJson(request)
} else { } else {
null null
} }
val coverImage = if (newCoverImageUri != null) { val coverImage = if (newCoverImageUri != null) {
val file = File(getRealPathFromURI(newCoverImageUri!!)) val file = File(getRealPathFromURI(newCoverImageUri))
MultipartBody.Part.createFormData( MultipartBody.Part.createFormData(
"coverImage", "coverImage",
file.name, file.name,
@ -481,7 +508,7 @@ class LiveRoomViewModel(
) )
} }
fun donation(roomId: Long, can: Int, message: String, onSuccess: () -> Unit) { fun donation(roomId: Long, can: Int, message: String, onSuccess: (String) -> 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}")
@ -492,7 +519,7 @@ class LiveRoomViewModel(
_isLoading.value = false _isLoading.value = false
if (it.success) { if (it.success) {
SharedPreferenceManager.can -= can SharedPreferenceManager.can -= can
onSuccess() onSuccess(it.data ?: "")
} else { } else {
if (it.message != null) { if (it.message != null) {
_toastLiveData.postValue(it.message) _toastLiveData.postValue(it.message)
@ -900,6 +927,40 @@ class LiveRoomViewModel(
) )
} }
fun getAllMenuPreset(onSuccess: (List<GetMenuPresetResponse>) -> Unit) {
_isLoading.value = true
compositeDisposable.add(
repository.getAllMenu(
creatorId = SharedPreferenceManager.userId,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success) {
onSuccess(it.data ?: listOf())
} 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("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
private fun randomSelectRouletteItem( private fun randomSelectRouletteItem(
can: Int, can: Int,
items: List<RouletteItem>, items: List<RouletteItem>,

View File

@ -257,7 +257,7 @@ data class LiveRoomDonationChat(
) )
), ),
0, 0,
chat.indexOf("", 0, true) + 2, chat.indexOf("", 0, true) + 1,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
) )

View File

@ -6,6 +6,7 @@ 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("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

@ -14,5 +14,8 @@ data class CreateLiveRoomRequest(
@SerializedName("beginDateTimeString") val beginDateTimeString: String? = null, @SerializedName("beginDateTimeString") val beginDateTimeString: String? = null,
@SerializedName("timezone") val timezone: String, @SerializedName("timezone") val timezone: String,
@SerializedName("type") val type: LiveRoomType, @SerializedName("type") val type: LiveRoomType,
@SerializedName("password") val password: String? = null @SerializedName("password") val password: String? = null,
@SerializedName("menuPanId") val menuPanId: Long = 0,
@SerializedName("menuPan") val menuPan: String = "",
@SerializedName("isActiveMenuPan") val isActiveMenuPan: Boolean = false
) )

View File

@ -9,6 +9,7 @@ import android.os.Handler
import android.os.Looper import android.os.Looper
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
@ -127,6 +128,8 @@ class LiveRoomCreateActivity : BaseActivity<ActivityLiveRoomCreateBinding>(
viewModel.setTimeNow( viewModel.setTimeNow(
intent.getBooleanExtra(Constants.EXTRA_LIVE_TIME_NOW, true) intent.getBooleanExtra(Constants.EXTRA_LIVE_TIME_NOW, true)
) )
viewModel.getAllMenuPreset()
} }
@SuppressLint("SetTextI18n", "ClickableViewAccessibility") @SuppressLint("SetTextI18n", "ClickableViewAccessibility")
@ -295,6 +298,30 @@ class LiveRoomCreateActivity : BaseActivity<ActivityLiveRoomCreateBinding>(
} }
false false
} }
binding.etMenu.setOnTouchListener { view, motionEvent ->
view.parent.parent.requestDisallowInterceptTouchEvent(true)
if ((motionEvent.action and MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP) {
view.parent.parent.requestDisallowInterceptTouchEvent(false)
}
false
}
binding.ivSwitch.setOnClickListener {
viewModel.toggleIsActivateMenu()
}
binding.llSelectMenu1.setOnClickListener {
viewModel.selectMenuPreset(LiveRoomCreateViewModel.SelectedMenu.MENU_1)
}
binding.llSelectMenu2.setOnClickListener {
viewModel.selectMenuPreset(LiveRoomCreateViewModel.SelectedMenu.MENU_2)
}
binding.llSelectMenu3.setOnClickListener {
viewModel.selectMenuPreset(LiveRoomCreateViewModel.SelectedMenu.MENU_3)
}
} }
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
@ -348,6 +375,15 @@ class LiveRoomCreateActivity : BaseActivity<ActivityLiveRoomCreateBinding>(
} }
) )
compositeDisposable.add(
binding.etMenu.textChanges().skip(1)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
viewModel.menu = it.toString()
}
)
viewModel.toastLiveData.observe(this) { viewModel.toastLiveData.observe(this) {
it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() } it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() }
} }
@ -573,6 +609,44 @@ class LiveRoomCreateActivity : BaseActivity<ActivityLiveRoomCreateBinding>(
else -> binding.rlPrice.isSelected = true else -> binding.rlPrice.isSelected = true
} }
} }
viewModel.isActivateMenuLiveData.observe(this) {
if (it) {
binding.llEditMenu.visibility = View.VISIBLE
binding.ivSwitch.setImageResource(R.drawable.btn_toggle_on_big)
} else {
binding.llEditMenu.visibility = View.GONE
binding.ivSwitch.setImageResource(R.drawable.btn_toggle_off_big)
}
}
viewModel.selectedMenuLiveData.observe(this) {
deselectAllMenuPreset()
when(it) {
LiveRoomCreateViewModel.SelectedMenu.MENU_2 -> selectMenuPresetButton(
binding.ivSelectMenu2,
binding.llSelectMenu2,
binding.tvSelectMenu2
)
LiveRoomCreateViewModel.SelectedMenu.MENU_3 -> selectMenuPresetButton(
binding.ivSelectMenu3,
binding.llSelectMenu3,
binding.tvSelectMenu3
)
else -> selectMenuPresetButton(
binding.ivSelectMenu1,
binding.llSelectMenu1,
binding.tvSelectMenu1
)
}
}
viewModel.menuLiveData.observe(this) {
binding.etMenu.setText(it)
}
} }
} }
@ -613,4 +687,69 @@ class LiveRoomCreateActivity : BaseActivity<ActivityLiveRoomCreateBinding>(
R.font.gmarket_sans_bold R.font.gmarket_sans_bold
) )
} }
private fun deselectAllMenuPreset() {
binding.ivSelectMenu1.visibility = View.GONE
binding.ivSelectMenu2.visibility = View.GONE
binding.ivSelectMenu3.visibility = View.GONE
binding.llSelectMenu1.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b)
binding.tvSelectMenu1.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_3bb9f1
)
)
if (viewModel.menuList.size > 0) {
binding.llSelectMenu2.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b)
binding.tvSelectMenu2.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_3bb9f1
)
)
} else {
binding.llSelectMenu2.setBackgroundResource(R.drawable.bg_round_corner_6_7_777777)
binding.tvSelectMenu2.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_555555
)
)
}
if (viewModel.menuList.size > 1) {
binding.llSelectMenu3.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b)
binding.tvSelectMenu3.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_3bb9f1
)
)
} else {
binding.llSelectMenu3.setBackgroundResource(R.drawable.bg_round_corner_6_7_777777)
binding.tvSelectMenu3.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_555555
)
)
}
}
private fun selectMenuPresetButton(
ivSelectMenuPreset: ImageView,
llSelectMenuPreset: LinearLayout,
tvSelectMenuPreset: TextView
) {
ivSelectMenuPreset.visibility = View.VISIBLE
llSelectMenuPreset.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
tvSelectMenuPreset.setTextColor(
ContextCompat.getColor(
applicationContext,
R.color.color_eeeeee
)
)
}
} }

View File

@ -11,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.LiveRoomType import kr.co.vividnext.sodalive.live.room.LiveRoomType
import kr.co.vividnext.sodalive.live.room.menu.GetMenuPresetResponse
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.asRequestBody
@ -22,6 +23,10 @@ class LiveRoomCreateViewModel(
private val repository: LiveRepository private val repository: LiveRepository
) : BaseViewModel() { ) : BaseViewModel() {
enum class SelectedMenu {
MENU_1, MENU_2, MENU_3
}
private val _roomTypeLiveData = MutableLiveData(LiveRoomType.OPEN) private val _roomTypeLiveData = MutableLiveData(LiveRoomType.OPEN)
val roomTypeLiveData: LiveData<LiveRoomType> val roomTypeLiveData: LiveData<LiveRoomType>
get() = _roomTypeLiveData get() = _roomTypeLiveData
@ -58,6 +63,18 @@ class LiveRoomCreateViewModel(
val isAdultLiveData: LiveData<Boolean> val isAdultLiveData: LiveData<Boolean>
get() = _isAdultLiveData get() = _isAdultLiveData
private val _selectedMenuLiveData = MutableLiveData<SelectedMenu>()
val selectedMenuLiveData: LiveData<SelectedMenu>
get() = _selectedMenuLiveData
private val _isActivateMenuLiveData = MutableLiveData(false)
val isActivateMenuLiveData: LiveData<Boolean>
get() = _isActivateMenuLiveData
private val _menuLiveData = MutableLiveData("")
val menuLiveData: LiveData<String>
get() = _menuLiveData
lateinit var getRealPathFromURI: (Uri) -> String? lateinit var getRealPathFromURI: (Uri) -> String?
var title = "" var title = ""
@ -70,6 +87,10 @@ class LiveRoomCreateViewModel(
var coverImagePath: String? = null var coverImagePath: String? = null
var password: String? = null var password: String? = null
private var menuId = 0L
var menu = ""
val menuList = mutableListOf<GetMenuPresetResponse>()
fun setRoomType(roomType: LiveRoomType) { fun setRoomType(roomType: LiveRoomType) {
if (_roomTypeLiveData.value!! != roomType) { if (_roomTypeLiveData.value!! != roomType) {
_roomTypeLiveData.postValue(roomType) _roomTypeLiveData.postValue(roomType)
@ -115,7 +136,18 @@ class LiveRoomCreateViewModel(
password password
} else { } else {
null null
} },
menuPanId = if (_isActivateMenuLiveData.value!!) {
menuId
} else {
0
},
menuPan = if (_isActivateMenuLiveData.value!!) {
menu
} else {
""
},
isActiveMenuPan = _isActivateMenuLiveData.value!!
) )
val requestJson = Gson().toJson(request) val requestJson = Gson().toJson(request)
@ -207,6 +239,11 @@ class LiveRoomCreateViewModel(
return false return false
} }
if (_isActivateMenuLiveData.value!! && menu.isBlank()) {
_toastLiveData.postValue("메뉴판은 빈칸일 수 없습니다.")
return false
}
return true return true
} }
@ -252,4 +289,80 @@ class LiveRoomCreateViewModel(
) )
) )
} }
fun selectMenuPreset(selectedMenuPreset: SelectedMenu) {
if (
menuList.isEmpty() &&
(
selectedMenuPreset == SelectedMenu.MENU_2 ||
selectedMenuPreset == SelectedMenu.MENU_3
)
) {
_toastLiveData.value = "메뉴 1을 먼저 설정하세요"
return
}
if (menuList.size == 1 && selectedMenuPreset == SelectedMenu.MENU_3) {
_toastLiveData.value = "메뉴 1과 메뉴 2를 먼저 설정하세요"
return
}
if (_selectedMenuLiveData.value != selectedMenuPreset) {
_selectedMenuLiveData.value = selectedMenuPreset
if (menuList.size > selectedMenuPreset.ordinal) {
val menuPreset = menuList[selectedMenuPreset.ordinal]
_menuLiveData.value = menuPreset.menu
menu = menuPreset.menu
menuId = menuPreset.id
} else {
_menuLiveData.value = ""
menu = ""
menuId = 0
}
}
}
fun toggleIsActivateMenu() {
_isActivateMenuLiveData.value = !_isActivateMenuLiveData.value!!
}
fun getAllMenuPreset() {
_isLoading.value = true
compositeDisposable.add(
repository.getAllMenu(
creatorId = SharedPreferenceManager.userId,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success) {
val data = it.data
menuList.clear()
menuList.addAll(data ?: listOf())
selectMenuPreset(SelectedMenu.MENU_1)
} 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

@ -21,6 +21,7 @@ data class GetRoomInfoResponse(
@SerializedName("listenerList") val listenerList: List<LiveRoomMember>, @SerializedName("listenerList") val listenerList: List<LiveRoomMember>,
@SerializedName("managerList") val managerList: List<LiveRoomMember>, @SerializedName("managerList") val managerList: List<LiveRoomMember>,
@SerializedName("donationRankingTop3UserIds") val donationRankingTop3UserIds: List<Long>, @SerializedName("donationRankingTop3UserIds") val donationRankingTop3UserIds: List<Long>,
@SerializedName("menuPan") val menuPan: String,
@SerializedName("isActiveRoulette") val isActiveRoulette: Boolean, @SerializedName("isActiveRoulette") val isActiveRoulette: Boolean,
@SerializedName("isPrivateRoom") val isPrivateRoom: Boolean, @SerializedName("isPrivateRoom") val isPrivateRoom: Boolean,
@SerializedName("password") val password: String? = null @SerializedName("password") val password: String? = null

View File

@ -0,0 +1,9 @@
package kr.co.vividnext.sodalive.live.room.menu
import com.google.gson.annotations.SerializedName
data class GetMenuPresetResponse(
@SerializedName("id") val id: Long,
@SerializedName("menu") val menu: String,
@SerializedName("isActive") val isActive: Boolean
)

View File

@ -0,0 +1,15 @@
package kr.co.vividnext.sodalive.live.room.menu
import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.common.ApiResponse
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Query
interface MenuApi {
@GET("/live/room/menu/all")
fun getAllMenu(
@Query("creatorId") creatorId: Long,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetMenuPresetResponse>>>
}

View File

@ -7,5 +7,8 @@ data class EditLiveRoomInfoRequest(
@SerializedName("notice") val notice: String?, @SerializedName("notice") val notice: String?,
@SerializedName("numberOfPeople") val numberOfPeople: Int?, @SerializedName("numberOfPeople") val numberOfPeople: Int?,
@SerializedName("beginDateTimeString") val beginDateTimeString: String?, @SerializedName("beginDateTimeString") val beginDateTimeString: String?,
@SerializedName("timezone") val timezone: String? @SerializedName("timezone") val timezone: String?,
@SerializedName("menuPanId") val menuPanId: Long = 0,
@SerializedName("menuPan") val menuPan: String = "",
@SerializedName("isActiveMenuPan") val isActiveMenuPan: Boolean? = null
) )

View File

@ -1,20 +1,32 @@
package kr.co.vividnext.sodalive.live.room.update package kr.co.vividnext.sodalive.live.room.update
import android.app.Activity import android.annotation.SuppressLint
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.net.Uri import android.net.Uri
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.WindowManager import android.view.WindowManager
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.lifecycle.MutableLiveData
import coil.load import coil.load
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.databinding.DialogLiveRoomInfoUpdateBinding import kr.co.vividnext.sodalive.databinding.DialogLiveRoomInfoUpdateBinding
import kr.co.vividnext.sodalive.extensions.dpToPx import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.live.room.create.LiveRoomCreateViewModel
import kr.co.vividnext.sodalive.live.room.menu.GetMenuPresetResponse
@SuppressLint("ClickableViewAccessibility")
class LiveRoomInfoEditDialog( class LiveRoomInfoEditDialog(
activity: Activity, private val activity: AppCompatActivity,
layoutInflater: LayoutInflater, layoutInflater: LayoutInflater,
onClickImagePicker: () -> Unit onClickImagePicker: () -> Unit
) { ) {
@ -24,6 +36,17 @@ class LiveRoomInfoEditDialog(
private var coverImageUrl: String? = null private var coverImageUrl: String? = null
private var coverImageUri: Uri? = null private var coverImageUri: Uri? = null
private var menuId = 0L
private val menuList = mutableListOf<GetMenuPresetResponse>()
private val isActivateMenuLiveData = MutableLiveData(false)
private val selectedMenuLiveData = MutableLiveData<LiveRoomCreateViewModel.SelectedMenu>()
private var menu: String = ""
private var isActivateMenu: Boolean? = null
private lateinit var selectedMenu: LiveRoomCreateViewModel.SelectedMenu
init { init {
val dialogBuilder = AlertDialog.Builder(activity) val dialogBuilder = AlertDialog.Builder(activity)
dialogBuilder.setView(dialogView.root) dialogBuilder.setView(dialogView.root)
@ -35,6 +58,77 @@ class LiveRoomInfoEditDialog(
dialogView.ivPhotoPicker.setOnClickListener { onClickImagePicker() } dialogView.ivPhotoPicker.setOnClickListener { onClickImagePicker() }
dialogView.ivClose.setOnClickListener { alertDialog.dismiss() } dialogView.ivClose.setOnClickListener { alertDialog.dismiss() }
dialogView.tvCancel.setOnClickListener { alertDialog.dismiss() } dialogView.tvCancel.setOnClickListener { alertDialog.dismiss() }
dialogView.etNotice.setOnTouchListener { view, motionEvent ->
view.parent.parent.requestDisallowInterceptTouchEvent(true)
if ((motionEvent.action and MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP) {
view.parent.parent.requestDisallowInterceptTouchEvent(false)
}
false
}
dialogView.etMenu.setOnTouchListener { view, motionEvent ->
view.parent.parent.requestDisallowInterceptTouchEvent(true)
if ((motionEvent.action and MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP) {
view.parent.parent.requestDisallowInterceptTouchEvent(false)
}
false
}
dialogView.ivSwitch.setOnClickListener {
isActivateMenuLiveData.value = !isActivateMenuLiveData.value!!
isActivateMenu = isActivateMenuLiveData.value!!
if (selectedMenuLiveData.value == null) {
selectMenuPreset(LiveRoomCreateViewModel.SelectedMenu.MENU_1)
}
}
dialogView.llSelectMenu1.setOnClickListener {
selectMenuPreset(LiveRoomCreateViewModel.SelectedMenu.MENU_1)
}
dialogView.llSelectMenu2.setOnClickListener {
selectMenuPreset(LiveRoomCreateViewModel.SelectedMenu.MENU_2)
}
dialogView.llSelectMenu3.setOnClickListener {
selectMenuPreset(LiveRoomCreateViewModel.SelectedMenu.MENU_3)
}
selectedMenuLiveData.observe(activity) {
deselectAllMenuPreset()
when (it) {
LiveRoomCreateViewModel.SelectedMenu.MENU_2 -> selectMenuPresetButton(
dialogView.ivSelectMenu2,
dialogView.llSelectMenu2,
dialogView.tvSelectMenu2
)
LiveRoomCreateViewModel.SelectedMenu.MENU_3 -> selectMenuPresetButton(
dialogView.ivSelectMenu3,
dialogView.llSelectMenu3,
dialogView.tvSelectMenu3
)
else -> selectMenuPresetButton(
dialogView.ivSelectMenu1,
dialogView.llSelectMenu1,
dialogView.tvSelectMenu1
)
}
}
isActivateMenuLiveData.observe(activity) {
if (it) {
dialogView.llEditMenu.visibility = View.VISIBLE
dialogView.ivSwitch.setImageResource(R.drawable.btn_toggle_on_big)
} else {
dialogView.llEditMenu.visibility = View.GONE
dialogView.ivSwitch.setImageResource(R.drawable.btn_toggle_off_big)
}
}
} }
fun setRoomInfo( fun setRoomInfo(
@ -42,7 +136,7 @@ class LiveRoomInfoEditDialog(
currentContent: String, currentContent: String,
) { ) {
dialogView.etTitle.setText(currentTitle) dialogView.etTitle.setText(currentTitle)
dialogView.etContent.setText(currentContent) dialogView.etNotice.setText(currentContent)
} }
fun setCoverImageUri(coverImageUri: Uri) { fun setCoverImageUri(coverImageUri: Uri) {
@ -63,13 +157,53 @@ class LiveRoomInfoEditDialog(
} }
} }
fun setConfirmAction(confirmAction: (String, String, Uri?) -> Unit) { fun setMenuPreset(menuList: List<GetMenuPresetResponse>) {
this.menuList.clear()
this.isActivateMenu = null
this.menuList.addAll(menuList)
this.selectedMenuLiveData.value = null
menuList.forEachIndexed { index, menuPreset ->
if (menuPreset.isActive) {
selectedMenu = when (index) {
1 -> LiveRoomCreateViewModel.SelectedMenu.MENU_2
2 -> LiveRoomCreateViewModel.SelectedMenu.MENU_3
else -> LiveRoomCreateViewModel.SelectedMenu.MENU_1
}
isActivateMenuLiveData.value = true
selectMenuPreset(selectedMenu)
}
}
}
fun setConfirmAction(confirmAction: (String, String, Uri?, Boolean?, Long, String) -> Unit) {
dialogView.tvConfirm.setOnClickListener { dialogView.tvConfirm.setOnClickListener {
alertDialog.dismiss() alertDialog.dismiss()
val newTitle = dialogView.etTitle.text.toString() val newTitle = dialogView.etTitle.text.toString()
val newContent = dialogView.etContent.text.toString() val newContent = dialogView.etNotice.text.toString()
confirmAction(newTitle, newContent, coverImageUri) val menu = dialogView.etMenu.text.toString()
confirmAction(
newTitle,
newContent,
coverImageUri,
if (isActivateMenu != null) {
isActivateMenu
} else if (
this.menu != menu ||
(
this::selectedMenu.isInitialized &&
this.selectedMenu != selectedMenuLiveData.value!!
)
) {
true
} else {
isActivateMenu
},
menuId,
menu
)
coverImageUri = null coverImageUri = null
coverImageUrl = null coverImageUrl = null
} }
@ -85,4 +219,103 @@ class LiveRoomInfoEditDialog(
alertDialog.window?.attributes = lp alertDialog.window?.attributes = lp
} }
private fun deselectAllMenuPreset() {
dialogView.ivSelectMenu1.visibility = View.GONE
dialogView.ivSelectMenu2.visibility = View.GONE
dialogView.ivSelectMenu3.visibility = View.GONE
dialogView.llSelectMenu1.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b)
dialogView.tvSelectMenu1.setTextColor(
ContextCompat.getColor(
activity,
R.color.color_3bb9f1
)
)
if (menuList.size > 0) {
dialogView.llSelectMenu2.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b)
dialogView.tvSelectMenu2.setTextColor(
ContextCompat.getColor(
activity,
R.color.color_3bb9f1
)
)
} else {
dialogView.llSelectMenu2.setBackgroundResource(R.drawable.bg_round_corner_6_7_777777)
dialogView.tvSelectMenu2.setTextColor(
ContextCompat.getColor(
activity,
R.color.color_555555
)
)
}
if (menuList.size > 1) {
dialogView.llSelectMenu3.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b)
dialogView.tvSelectMenu3.setTextColor(
ContextCompat.getColor(
activity,
R.color.color_3bb9f1
)
)
} else {
dialogView.llSelectMenu3.setBackgroundResource(R.drawable.bg_round_corner_6_7_777777)
dialogView.tvSelectMenu3.setTextColor(
ContextCompat.getColor(
activity,
R.color.color_555555
)
)
}
}
private fun selectMenuPresetButton(
ivSelectMenuPreset: ImageView,
llSelectMenuPreset: LinearLayout,
tvSelectMenuPreset: TextView
) {
ivSelectMenuPreset.visibility = View.VISIBLE
llSelectMenuPreset.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
tvSelectMenuPreset.setTextColor(
ContextCompat.getColor(
activity,
R.color.color_eeeeee
)
)
}
private fun selectMenuPreset(selectedMenuPreset: LiveRoomCreateViewModel.SelectedMenu) {
if (
menuList.isEmpty() &&
(
selectedMenuPreset == LiveRoomCreateViewModel.SelectedMenu.MENU_2 ||
selectedMenuPreset == LiveRoomCreateViewModel.SelectedMenu.MENU_3
)
) {
Toast.makeText(activity, "메뉴 1을 먼저 설정하세요", Toast.LENGTH_SHORT).show()
return
}
if (menuList.size == 1 && selectedMenuPreset == LiveRoomCreateViewModel.SelectedMenu.MENU_3) {
Toast.makeText(activity, "메뉴 1과 메뉴 2를 먼저 설정하세요", Toast.LENGTH_SHORT).show()
return
}
if (selectedMenuLiveData.value != selectedMenuPreset) {
selectedMenuLiveData.value = selectedMenuPreset
if (menuList.size > selectedMenuPreset.ordinal) {
val menuPreset = menuList[selectedMenuPreset.ordinal]
menu = menuPreset.menu
menuId = menuPreset.id
dialogView.etMenu.setText(menuPreset.menu)
} else {
menu = ""
menuId = 0
dialogView.etMenu.setText("")
}
}
}
} }

View File

@ -0,0 +1,10 @@
package kr.co.vividnext.sodalive.live.roulette
import com.google.gson.annotations.SerializedName
data class GetNewRouletteResponse(
@SerializedName("id") val id: Long,
@SerializedName("can") val can: Int,
@SerializedName("isActive") val isActive: Boolean,
@SerializedName("items") val items: List<RouletteItem>
)

View File

@ -2,18 +2,32 @@ package kr.co.vividnext.sodalive.live.roulette
import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.live.roulette.config.CreateOrUpdateRouletteRequest import kr.co.vividnext.sodalive.live.roulette.config.CreateRouletteRequest
import kr.co.vividnext.sodalive.live.roulette.config.RouletteApi import kr.co.vividnext.sodalive.live.roulette.config.RouletteApi
import kr.co.vividnext.sodalive.live.roulette.config.UpdateRouletteRequest
class RouletteRepository(private val api: RouletteApi) { class RouletteRepository(private val api: RouletteApi) {
fun createOrUpdateRoulette( fun createRoulette(
request: CreateOrUpdateRouletteRequest, request: CreateRouletteRequest,
token: String token: String
) = api.createOrUpdateRoulette( ) = api.createRoulette(
request = request, request = request,
authHeader = token authHeader = token
) )
fun updateRoulette(
request: UpdateRouletteRequest,
token: String
) = api.updateRoulette(
request = request,
authHeader = token
)
fun getAllRoulette(creatorId: Long, token: String) = api.getAllRoulette(
creatorId = creatorId,
authHeader = token
)
fun getRoulette(creatorId: Long, token: String) = api.getRoulette( fun getRoulette(creatorId: Long, token: String) = api.getRoulette(
creatorId = creatorId, creatorId = creatorId,
authHeader = token authHeader = token

View File

@ -3,7 +3,7 @@ package kr.co.vividnext.sodalive.live.roulette.config
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.live.roulette.RouletteItem import kr.co.vividnext.sodalive.live.roulette.RouletteItem
data class CreateOrUpdateRouletteRequest( data class CreateRouletteRequest(
@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>

View File

@ -2,35 +2,49 @@ package kr.co.vividnext.sodalive.live.roulette.config
import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.ApiResponse
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 retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Header import retrofit2.http.Header
import retrofit2.http.POST import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Path import retrofit2.http.Path
import retrofit2.http.Query import retrofit2.http.Query
interface RouletteApi { interface RouletteApi {
@POST("/roulette") @POST("/new-roulette")
fun createOrUpdateRoulette( fun createRoulette(
@Body request: CreateOrUpdateRouletteRequest, @Body request: CreateRouletteRequest,
@Header("Authorization") authHeader: String @Header("Authorization") authHeader: String
): Single<ApiResponse<Any>> ): Single<ApiResponse<Any>>
@GET("/roulette") @PUT("/new-roulette")
fun updateRoulette(
@Body request: UpdateRouletteRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@GET("/new-roulette/creator")
fun getAllRoulette(
@Query("creatorId") creatorId: Long,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<GetNewRouletteResponse>>>
@GET("/new-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<GetRouletteResponse>>
@POST("/roulette/spin") @POST("/new-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<GetRouletteResponse>>
@POST("/roulette/refund/{id}") @POST("/new-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

@ -13,7 +13,9 @@ import android.view.View
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.EditText import android.widget.EditText
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.core.content.ContextCompat
import com.jakewharton.rxbinding4.widget.textChanges import com.jakewharton.rxbinding4.widget.textChanges
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
@ -41,7 +43,7 @@ class RouletteSettingsFragment : BaseFragment<FragmentRouletteSettingsBinding>(
setupView() setupView()
bindData() bindData()
viewModel.getRoulette() viewModel.getAllRoulette()
} }
private fun setupView() { private fun setupView() {
@ -74,15 +76,57 @@ class RouletteSettingsFragment : BaseFragment<FragmentRouletteSettingsBinding>(
handler.postDelayed({ handler.postDelayed({
imm.hideSoftInputFromWindow(view?.windowToken, 0) imm.hideSoftInputFromWindow(view?.windowToken, 0)
}, 100) }, 100)
viewModel.createOrUpdateRoulette { viewModel.createOrUpdateRoulette {
val resultIntent = Intent().apply { putExtra(Constants.EXTRA_RESULT_ROULETTE, it) } val resultIntent = Intent().apply { putExtra(Constants.EXTRA_RESULT_ROULETTE, it) }
requireActivity().setResult(Activity.RESULT_OK, resultIntent) requireActivity().setResult(Activity.RESULT_OK, resultIntent)
requireActivity().finish() requireActivity().finish()
} }
} }
binding.llSelectRoulette1.setOnClickListener {
viewModel.selectRoulette(
RouletteSettingsViewModel.SelectedRoulette.ROULETTE_1
)
}
binding.llSelectRoulette2.setOnClickListener {
viewModel.selectRoulette(
RouletteSettingsViewModel.SelectedRoulette.ROULETTE_2
)
}
binding.llSelectRoulette3.setOnClickListener {
viewModel.selectRoulette(
RouletteSettingsViewModel.SelectedRoulette.ROULETTE_3
)
}
} }
private fun bindData() { private fun bindData() {
viewModel.selectedRouletteLiveData.observe(viewLifecycleOwner) {
deselectAllRoulette()
when (it) {
RouletteSettingsViewModel.SelectedRoulette.ROULETTE_2 -> selectRouletteButton(
binding.ivSelectRoulette2,
binding.llSelectRoulette2,
binding.tvSelectRoulette2
)
RouletteSettingsViewModel.SelectedRoulette.ROULETTE_3 -> selectRouletteButton(
binding.ivSelectRoulette3,
binding.llSelectRoulette3,
binding.tvSelectRoulette3
)
else -> selectRouletteButton(
binding.ivSelectRoulette1,
binding.llSelectRoulette1,
binding.tvSelectRoulette1
)
}
}
viewModel.isActiveLiveData.observe(viewLifecycleOwner) { viewModel.isActiveLiveData.observe(viewLifecycleOwner) {
binding.ivRouletteIsActive.setImageResource( binding.ivRouletteIsActive.setImageResource(
if (it) R.drawable.btn_toggle_on_big else R.drawable.btn_toggle_off_big if (it) R.drawable.btn_toggle_on_big else R.drawable.btn_toggle_off_big
@ -185,4 +229,69 @@ class RouletteSettingsFragment : BaseFragment<FragmentRouletteSettingsBinding>(
return optionView return optionView
} }
private fun deselectAllRoulette() {
binding.ivSelectRoulette1.visibility = View.GONE
binding.ivSelectRoulette2.visibility = View.GONE
binding.ivSelectRoulette3.visibility = View.GONE
binding.llSelectRoulette1.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b)
binding.tvSelectRoulette1.setTextColor(
ContextCompat.getColor(
requireContext(),
R.color.color_3bb9f1
)
)
if (viewModel.rouletteList.size > 0) {
binding.llSelectRoulette2.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b)
binding.tvSelectRoulette2.setTextColor(
ContextCompat.getColor(
requireContext(),
R.color.color_3bb9f1
)
)
} else {
binding.llSelectRoulette2.setBackgroundResource(R.drawable.bg_round_corner_6_7_777777)
binding.tvSelectRoulette2.setTextColor(
ContextCompat.getColor(
requireContext(),
R.color.color_555555
)
)
}
if (viewModel.rouletteList.size > 1) {
binding.llSelectRoulette3.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b)
binding.tvSelectRoulette3.setTextColor(
ContextCompat.getColor(
requireContext(),
R.color.color_3bb9f1
)
)
} else {
binding.llSelectRoulette3.setBackgroundResource(R.drawable.bg_round_corner_6_7_777777)
binding.tvSelectRoulette3.setTextColor(
ContextCompat.getColor(
requireContext(),
R.color.color_555555
)
)
}
}
private fun selectRouletteButton(
ivSelectRoulette: ImageView,
llSelectRoulette: LinearLayout,
tvSelectRoulette: TextView
) {
ivSelectRoulette.visibility = View.VISIBLE
llSelectRoulette.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1)
tvSelectRoulette.setTextColor(
ContextCompat.getColor(
requireContext(),
R.color.color_eeeeee
)
)
}
} }

View File

@ -7,6 +7,7 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
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
import kr.co.vividnext.sodalive.live.roulette.GetNewRouletteResponse
import kr.co.vividnext.sodalive.live.roulette.RouletteItem 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
@ -15,6 +16,10 @@ import kotlin.math.floor
class RouletteSettingsViewModel(private val repository: RouletteRepository) : BaseViewModel() { class RouletteSettingsViewModel(private val repository: RouletteRepository) : BaseViewModel() {
enum class SelectedRoulette {
ROULETTE_1, ROULETTE_2, ROULETTE_3
}
private var _isLoading = MutableLiveData(false) private var _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean> val isLoading: LiveData<Boolean>
get() = _isLoading get() = _isLoading
@ -39,9 +44,16 @@ class RouletteSettingsViewModel(private val repository: RouletteRepository) : Ba
val roulettePreviewLiveData: LiveData<RoulettePreview> val roulettePreviewLiveData: LiveData<RoulettePreview>
get() = _roulettePreviewLiveData get() = _roulettePreviewLiveData
private val options = mutableListOf<RouletteOption>() private val _selectedRouletteLiveData = MutableLiveData<SelectedRoulette>()
val selectedRouletteLiveData: LiveData<SelectedRoulette>
get() = _selectedRouletteLiveData
var can = 0 var can = 0
var isActive = false var isActive = false
private var rouletteId = 0L
private val options = mutableListOf<RouletteOption>()
val rouletteList = mutableListOf<GetNewRouletteResponse>()
fun plusWeight(optionIndex: Int) { fun plusWeight(optionIndex: Int) {
val currentOption = options[optionIndex] val currentOption = options[optionIndex]
@ -113,66 +125,166 @@ class RouletteSettingsViewModel(private val repository: RouletteRepository) : Ba
if (!_isLoading.value!!) { if (!_isLoading.value!!) {
_isLoading.value = true _isLoading.value = true
val items = mutableListOf<RouletteItem>() if (rouletteId > 0) {
for (option in options) { updateRoulette(onSuccess)
if (option.title.trim().isEmpty()) { } else {
_toastLiveData.value = "옵션은 빈칸을 할 수 없습니다." createRoulette(onSuccess)
_isLoading.value = false
return
}
items.add(RouletteItem(title = option.title, weight = option.weight))
} }
val request = CreateOrUpdateRouletteRequest(
can = can,
isActive = isActive,
items = items
)
compositeDisposable.add(
repository.createOrUpdateRoulette(
request = request,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success && it.data != null && it.data is Boolean) {
val message = if (it.data) {
"룰렛을 활성화 했습니다."
} else {
"룰렛을 비활성화 했습니다."
}
_toastLiveData.postValue(message)
onSuccess(it.data)
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
} }
} }
fun getRoulette() { private fun updateRoulette(onSuccess: (Boolean) -> Unit) {
val items = mutableListOf<RouletteItem>()
for (option in options) {
if (option.title.trim().isEmpty()) {
_toastLiveData.value = "옵션은 빈칸을 할 수 없습니다."
_isLoading.value = false
return
}
items.add(RouletteItem(title = option.title, weight = option.weight))
}
val selectedRoulette = rouletteList[_selectedRouletteLiveData.value!!.ordinal]
if (
selectedRoulette.isActive == isActive &&
selectedRoulette.can == can &&
selectedRoulette.items == items
) {
_toastLiveData.value = "변동사항이 없습니다."
_isLoading.value = false
return
}
val request = UpdateRouletteRequest(
id = rouletteId,
can = can,
isActive = isActive,
items = items
)
val selectedRouletteTitle = when (_selectedRouletteLiveData.value!!) {
SelectedRoulette.ROULETTE_1 -> "룰렛 1"
SelectedRoulette.ROULETTE_2 -> "룰렛 2"
SelectedRoulette.ROULETTE_3 -> "룰렛 3"
}
var isAllActive = false
rouletteList
.filter {
it.id != selectedRoulette.id
}
.forEach {
if (it.isActive) {
isAllActive = true
}
}
val successMessage = if (isActive) {
"${selectedRouletteTitle}로 설정하였습니다."
} else if (!isAllActive) {
"${selectedRouletteTitle}이 비활성화 되었습니다."
} else {
"${selectedRouletteTitle}을 설정했습니다."
}
compositeDisposable.add(
repository.updateRoulette(
request = request,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success && it.data != null && it.data is Boolean) {
_toastLiveData.postValue(successMessage)
onSuccess(it.data)
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
private fun createRoulette(onSuccess: (Boolean) -> Unit) {
val items = mutableListOf<RouletteItem>()
for (option in options) {
if (option.title.trim().isEmpty()) {
_toastLiveData.value = "옵션은 빈칸을 할 수 없습니다."
_isLoading.value = false
return
}
items.add(RouletteItem(title = option.title, weight = option.weight))
}
val request = CreateRouletteRequest(
can = can,
isActive = isActive,
items = items
)
val selectedRouletteTitle = when (_selectedRouletteLiveData.value!!) {
SelectedRoulette.ROULETTE_1 -> "룰렛 1"
SelectedRoulette.ROULETTE_2 -> "룰렛 2"
SelectedRoulette.ROULETTE_3 -> "룰렛 3"
}
val successMessage = "$selectedRouletteTitle " +
if (isActive) "로 설정하였습니다." else "을 설정했습니다."
compositeDisposable.add(
repository.createRoulette(
request = request,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success && it.data != null && it.data is Boolean) {
_toastLiveData.postValue(successMessage)
onSuccess(it.data)
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
"알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
)
}
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
fun getAllRoulette() {
if (!_isLoading.value!!) { if (!_isLoading.value!!) {
_isLoading.value = true _isLoading.value = true
compositeDisposable.add( compositeDisposable.add(
repository.getRoulette( repository.getAllRoulette(
creatorId = SharedPreferenceManager.userId, creatorId = SharedPreferenceManager.userId,
token = "Bearer ${SharedPreferenceManager.token}" token = "Bearer ${SharedPreferenceManager.token}"
) )
@ -182,30 +294,9 @@ class RouletteSettingsViewModel(private val repository: RouletteRepository) : Ba
{ {
if (it.success) { if (it.success) {
val data = it.data val data = it.data
rouletteList.clear()
if (data != null && data.items.isNotEmpty()) { rouletteList.addAll(data ?: listOf())
_isActiveLiveData.value = data.isActive selectRoulette(SelectedRoulette.ROULETTE_1)
_canLiveData.value = data.can
isActive = data.isActive
can = data.can
val options = data.items.asSequence().map { item ->
RouletteOption(title = item.title, weight = item.weight)
}.toList()
removeAllAndAddOptions(options = options)
recalculatePercentages(options)
} else {
_isActiveLiveData.value = false
_canLiveData.value = 0
isActive = false
can = 0
options.add(RouletteOption(title = "", weight = 1))
options.add(RouletteOption(title = "", weight = 1))
recalculatePercentages(options)
}
} else { } else {
if (it.message != null) { if (it.message != null) {
_toastLiveData.postValue(it.message) _toastLiveData.postValue(it.message)
@ -227,6 +318,56 @@ class RouletteSettingsViewModel(private val repository: RouletteRepository) : Ba
} }
} }
fun selectRoulette(selectedRoulette: SelectedRoulette) {
if (
rouletteList.isEmpty() &&
(
selectedRoulette == SelectedRoulette.ROULETTE_2 ||
selectedRoulette == SelectedRoulette.ROULETTE_3
)
) {
_toastLiveData.value = "룰렛 1을 먼저 설정하세요"
return
}
if (rouletteList.size == 1 && selectedRoulette == SelectedRoulette.ROULETTE_3) {
_toastLiveData.value = "룰렛 1과 룰렛 2를 먼저 설정하세요"
return
}
if (_selectedRouletteLiveData.value != selectedRoulette) {
_selectedRouletteLiveData.value = selectedRoulette
if (rouletteList.size > selectedRoulette.ordinal) {
val roulette = rouletteList[selectedRoulette.ordinal]
_canLiveData.value = roulette.can
_isActiveLiveData.value = roulette.isActive
can = roulette.can
rouletteId = roulette.id
isActive = roulette.isActive
val options = roulette.items.asSequence().map { item ->
RouletteOption(title = item.title, weight = item.weight)
}.toList()
removeAllAndAddOptions(options = options)
recalculatePercentages(options)
} else {
_canLiveData.value = 0
_isActiveLiveData.value = false
can = 0
rouletteId = 0
isActive = false
options.clear()
options.add(RouletteOption(title = "", weight = 1))
options.add(RouletteOption(title = "", weight = 1))
recalculatePercentages(options)
}
}
}
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)

View File

@ -0,0 +1,11 @@
package kr.co.vividnext.sodalive.live.roulette.config
import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.live.roulette.RouletteItem
data class UpdateRouletteRequest(
@SerializedName("id") val id: Long,
@SerializedName("can") val can: Int,
@SerializedName("isActive") val isActive: Boolean,
@SerializedName("items") val items: List<RouletteItem>
)

View File

@ -0,0 +1,7 @@
package kr.co.vividnext.sodalive.main
import com.google.gson.annotations.SerializedName
data class GaidUpdateRequest(
@SerializedName("adid") val adid: String
)

View File

@ -59,6 +59,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
checkPermissions() checkPermissions()
pushTokenUpdate() pushTokenUpdate()
gaidUpdate()
getMemberInfo() getMemberInfo()
getEventPopup() getEventPopup()
@ -338,6 +339,10 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
} }
} }
private fun gaidUpdate() {
viewModel.fetchAndUpdateGaid(context = applicationContext)
}
private fun getMemberInfo() { private fun getMemberInfo() {
viewModel.getMemberInfo { viewModel.getMemberInfo {
notificationSettingsDialog.show(screenWidth) notificationSettingsDialog.show(screenWidth)

View File

@ -1,7 +1,9 @@
package kr.co.vividnext.sodalive.main package kr.co.vividnext.sodalive.main
import android.content.Context
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.google.android.gms.ads.identifier.AdvertisingIdClient
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
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
@ -15,6 +17,7 @@ import kr.co.vividnext.sodalive.settings.event.EventItem
import kr.co.vividnext.sodalive.settings.event.EventRepository import kr.co.vividnext.sodalive.settings.event.EventRepository
import kr.co.vividnext.sodalive.settings.notification.UpdateNotificationSettingRequest import kr.co.vividnext.sodalive.settings.notification.UpdateNotificationSettingRequest
import kr.co.vividnext.sodalive.user.UserRepository import kr.co.vividnext.sodalive.user.UserRepository
import java.util.concurrent.Executors
class MainViewModel( class MainViewModel(
private val userRepository: UserRepository, private val userRepository: UserRepository,
@ -153,4 +156,26 @@ class MainViewModel(
) )
) )
} }
fun fetchAndUpdateGaid(context: Context) {
Executors.newSingleThreadExecutor().execute {
try {
val adInfo = AdvertisingIdClient.getAdvertisingIdInfo(context)
adInfo.id?.let { gaid ->
val request = GaidUpdateRequest(adid = gaid)
compositeDisposable.add(
userRepository.updateGaid(
request,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({}, {})
)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
} }

View File

@ -200,7 +200,6 @@ class VoiceMessageFragment : BaseFragment<FragmentVoiceMessageBinding>(
override fun run() { override fun run() {
handler.post { handler.post {
seekbar.progress = mediaPlayer?.currentPosition ?: 0 seekbar.progress = mediaPlayer?.currentPosition ?: 0
Logger.e("test")
} }
} }
} }

View File

@ -2,10 +2,11 @@ package kr.co.vividnext.sodalive.mypage.can
import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.mypage.can.charge.CanResponse import kr.co.vividnext.sodalive.mypage.can.charge.iap.GoogleChargeRequest
import kr.co.vividnext.sodalive.mypage.can.charge.ChargeRequest import kr.co.vividnext.sodalive.mypage.can.charge.pg.CanResponse
import kr.co.vividnext.sodalive.mypage.can.charge.ChargeResponse import kr.co.vividnext.sodalive.mypage.can.charge.pg.ChargeRequest
import kr.co.vividnext.sodalive.mypage.can.charge.VerifyRequest import kr.co.vividnext.sodalive.mypage.can.charge.pg.ChargeResponse
import kr.co.vividnext.sodalive.mypage.can.charge.pg.VerifyRequest
import kr.co.vividnext.sodalive.mypage.can.coupon.UseCanCouponRequest import kr.co.vividnext.sodalive.mypage.can.coupon.UseCanCouponRequest
import kr.co.vividnext.sodalive.mypage.can.status.GetCanStatusResponse import kr.co.vividnext.sodalive.mypage.can.status.GetCanStatusResponse
import kr.co.vividnext.sodalive.mypage.can.status.charge.GetCanChargeStatusResponseItem import kr.co.vividnext.sodalive.mypage.can.status.charge.GetCanChargeStatusResponseItem
@ -17,6 +18,12 @@ import retrofit2.http.POST
import retrofit2.http.Query import retrofit2.http.Query
interface CanApi { interface CanApi {
@POST("/charge/google")
fun googleChargeCan(
@Body request: GoogleChargeRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@POST("/charge") @POST("/charge")
fun chargeCan( fun chargeCan(
@Body chargeRequest: ChargeRequest, @Body chargeRequest: ChargeRequest,

View File

@ -1,11 +1,17 @@
package kr.co.vividnext.sodalive.mypage.can package kr.co.vividnext.sodalive.mypage.can
import kr.co.vividnext.sodalive.mypage.can.charge.ChargeRequest import kr.co.vividnext.sodalive.mypage.can.charge.iap.GoogleChargeRequest
import kr.co.vividnext.sodalive.mypage.can.charge.VerifyRequest import kr.co.vividnext.sodalive.mypage.can.charge.pg.ChargeRequest
import kr.co.vividnext.sodalive.mypage.can.charge.pg.VerifyRequest
import kr.co.vividnext.sodalive.mypage.can.coupon.UseCanCouponRequest import kr.co.vividnext.sodalive.mypage.can.coupon.UseCanCouponRequest
import java.util.TimeZone import java.util.TimeZone
class CanRepository(private val api: CanApi) { class CanRepository(private val api: CanApi) {
fun googleChargeCan(
request: GoogleChargeRequest,
token: String
) = api.googleChargeCan(request, authHeader = token)
fun chargeCan( fun chargeCan(
chargeRequest: ChargeRequest, chargeRequest: ChargeRequest,
token: String token: String

View File

@ -1,33 +1,28 @@
package kr.co.vividnext.sodalive.mypage.can.charge package kr.co.vividnext.sodalive.mypage.can.charge
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.graphics.Rect
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.tabs.TabLayout
import androidx.recyclerview.widget.RecyclerView 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.LoadingDialog 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.extensions.dpToPx 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.CanResponse
import kr.co.vividnext.sodalive.mypage.can.payment.CanPaymentActivity import kr.co.vividnext.sodalive.mypage.can.payment.CanPaymentActivity
import org.koin.android.ext.android.inject import kr.co.vividnext.sodalive.mypage.can.status.CanStatusActivity
class CanChargeActivity : BaseActivity<ActivityCanChargeBinding>( class CanChargeActivity : BaseActivity<ActivityCanChargeBinding>(
ActivityCanChargeBinding::inflate ActivityCanChargeBinding::inflate
) { ) {
private val viewModel: CanChargeViewModel by inject()
private var gotoPrevPage: Boolean = false private var gotoPrevPage: Boolean = false
private lateinit var adapter: CanChargeAdapter
private lateinit var loadingDialog: LoadingDialog
private lateinit var activityResultLauncher: ActivityResultLauncher<Intent> private lateinit var activityResultLauncher: ActivityResultLauncher<Intent>
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -40,87 +35,71 @@ class CanChargeActivity : BaseActivity<ActivityCanChargeBinding>(
} }
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
bindData()
viewModel.getCanCharges()
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.text = "충전하기"
binding.toolbar.tvBack.setOnClickListener { finish() }
gotoPrevPage = intent.getBooleanExtra( gotoPrevPage = intent.getBooleanExtra(
Constants.EXTRA_GO_TO_PREV_PAGE, Constants.EXTRA_GO_TO_PREV_PAGE,
false false
) )
val recyclerView = binding.rvChargeCan supportFragmentManager.beginTransaction()
adapter = CanChargeAdapter { .replace(R.id.fl_container, CanChargeIapFragment()).commit()
val intent = Intent(applicationContext, CanPaymentActivity::class.java) }
intent.putExtra(Constants.EXTRA_CAN, it)
intent.putExtra(Constants.EXTRA_GO_TO_PREV_PAGE, gotoPrevPage)
activityResultLauncher.launch(intent)
}
recyclerView.layoutManager = LinearLayoutManager( override fun setupView() {
applicationContext, setupToolbar()
LinearLayoutManager.VERTICAL, setupTabs()
false }
)
recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() { private fun setupToolbar() {
override fun getItemOffsets( binding.toolbar.tvBack.text = "충전하기"
outRect: Rect, binding.toolbar.tvBack.setOnClickListener { finish() }
view: View, }
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
outRect.left = 13.3f.dpToPx().toInt() private fun setupTabs() {
outRect.right = 13.3f.dpToPx().toInt() if (SharedPreferenceManager.isAuth) {
val tabs = binding.tabs
when (parent.getChildAdapterPosition(view)) { tabs.visibility = View.VISIBLE
0 -> { tabs.addTab(tabs.newTab().setText("인 앱 결제"))
outRect.top = 13.3f.dpToPx().toInt() tabs.addTab(tabs.newTab().setText("PG"))
outRect.bottom = 6.7f.dpToPx().toInt()
}
adapter.itemCount - 1 -> { tabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
outRect.top = 6.7f.dpToPx().toInt() override fun onTabSelected(tab: TabLayout.Tab) {
outRect.bottom = 26.7f.dpToPx().toInt() when (tab.position) {
} 0 -> supportFragmentManager.beginTransaction()
.replace(R.id.fl_container, CanChargeIapFragment()).commit()
else -> { 1 -> supportFragmentManager.beginTransaction()
outRect.top = 6.7f.dpToPx().toInt() .replace(R.id.fl_container, CanChargePgFragment()).commit()
outRect.bottom = 6.7f.dpToPx().toInt()
} }
} }
}
})
recyclerView.adapter = adapter override fun onTabUnselected(tab: TabLayout.Tab) {
}
override fun onTabReselected(tab: TabLayout.Tab) {
}
})
} else {
binding.tabs.visibility = View.GONE
}
} }
@SuppressLint("NotifyDataSetChanged") fun selectCan(model: CanResponse) {
private fun bindData() { val intent = Intent(applicationContext, CanPaymentActivity::class.java)
viewModel.canChargeLiveData.observe(this) { intent.putExtra(Constants.EXTRA_CAN, model)
adapter.items.addAll(it) intent.putExtra(Constants.EXTRA_GO_TO_PREV_PAGE, gotoPrevPage)
adapter.notifyDataSetChanged() activityResultLauncher.launch(intent)
}
fun successIapCharge() {
if (gotoPrevPage) {
setResult(RESULT_OK)
} else {
val intent = Intent(applicationContext, CanStatusActivity::class.java)
startActivity(intent)
} }
viewModel.toastLiveData.observe(this) { finish()
it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() }
}
viewModel.isLoading.observe(this) {
if (it) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
}
}
} }
} }

View File

@ -0,0 +1,57 @@
package kr.co.vividnext.sodalive.mypage.can.charge.iap
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.content.res.ResourcesCompat
import androidx.recyclerview.widget.RecyclerView
import com.android.billingclient.api.ProductDetails
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemCanChargeBinding
import kr.co.vividnext.sodalive.extensions.fontSpan
class CanChargeIapAdapter(
private val onClick: (ProductDetails) -> Unit
) : RecyclerView.Adapter<CanChargeIapAdapter.ViewHolder>() {
val items = mutableListOf<ProductDetails>()
inner class ViewHolder(
private val context: Context,
private val binding: ItemCanChargeBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: ProductDetails) {
binding.tvPrice.text = item.oneTimePurchaseOfferDetails?.formattedPrice
val typeface = ResourcesCompat.getFont(context, R.font.gmarket_sans_medium)
binding.tvTitle.text = item.name.fontSpan(
typeface,
""
)
binding.root.setOnClickListener { onClick(item) }
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
parent.context,
ItemCanChargeBinding.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<ProductDetails>) {
this.items.addAll(items.sortedBy { it.description.toInt() })
notifyDataSetChanged()
}
}

View File

@ -0,0 +1,282 @@
package kr.co.vividnext.sodalive.mypage.can.charge.iap
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Rect
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.View
import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.android.billingclient.api.BillingClient
import com.android.billingclient.api.BillingClientStateListener
import com.android.billingclient.api.BillingFlowParams
import com.android.billingclient.api.BillingResult
import com.android.billingclient.api.ConsumeParams
import com.android.billingclient.api.ProductDetails
import com.android.billingclient.api.Purchase
import com.android.billingclient.api.PurchasesUpdatedListener
import com.android.billingclient.api.QueryProductDetailsParams
import com.android.billingclient.api.QueryPurchasesParams
import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.FragmentCanChargeIapBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeActivity
import org.koin.android.ext.android.inject
class CanChargeIapFragment : BaseFragment<FragmentCanChargeIapBinding>(
FragmentCanChargeIapBinding::inflate
) {
private val viewModel: CanChargeIapViewModel by inject()
private lateinit var adapter: CanChargeIapAdapter
private lateinit var loadingDialog: LoadingDialog
private lateinit var billingClient: BillingClient
private val handler = Handler(Looper.getMainLooper())
private var selectedProductDetails: ProductDetails? = null
private lateinit var purchaseUpdateListener: PurchasesUpdatedListener
fun safeContext(): Context? {
return if (isAdded) context else null
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
bindData()
setupRecyclerView()
setupBillingClient()
}
override fun onStart() {
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()
}
private fun bindData() {
viewModel.isLoading.observe(viewLifecycleOwner) {
if (it) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
}
}
viewModel.toastLiveData.observe(viewLifecycleOwner) {
Toast.makeText(requireActivity(), it, Toast.LENGTH_LONG).show()
}
}
private fun setupRecyclerView() {
val recyclerView = binding.rvChargeCan
adapter = CanChargeIapAdapter { productDetails ->
selectedProductDetails = productDetails
launchPurchaseFlow(productDetails)
}
recyclerView.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.VERTICAL,
false
)
recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
outRect.left = 13.3f.dpToPx().toInt()
outRect.right = 13.3f.dpToPx().toInt()
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.top = 13.3f.dpToPx().toInt()
outRect.bottom = 6.7f.dpToPx().toInt()
}
adapter.itemCount - 1 -> {
outRect.top = 6.7f.dpToPx().toInt()
outRect.bottom = 26.7f.dpToPx().toInt()
}
else -> {
outRect.top = 6.7f.dpToPx().toInt()
outRect.bottom = 6.7f.dpToPx().toInt()
}
}
}
})
recyclerView.adapter = adapter
}
private fun setupBillingClient() {
purchaseUpdateListener = PurchasesUpdatedListener { billingResult, purchases ->
handler.post {
if (
billingResult.responseCode == BillingClient.BillingResponseCode.OK &&
purchases != null &&
selectedProductDetails != null
) {
for (purchase in purchases) {
handlePurchase(purchase)
}
} else if (
billingResult.responseCode == BillingClient.BillingResponseCode.USER_CANCELED
) {
selectedProductDetails = null
viewModel.showToast("구매를 취소했습니다.")
} else {
selectedProductDetails = null
viewModel.showToast("구매를 하지 못했습니다.\n다시 시도해 주세요.")
}
}
}
billingClient = BillingClient.newBuilder(requireActivity())
.enablePendingPurchases()
.setListener(purchaseUpdateListener)
.build()
loadingDialog.show(screenWidth)
}
@SuppressLint("NotifyDataSetChanged")
private fun queryAvailableCans() {
val productList = listOf(
"${requireContext().packageName}.can_35",
"${requireContext().packageName}.can_55",
"${requireContext().packageName}.can_105",
"${requireContext().packageName}.can_350",
"${requireContext().packageName}.can_550",
"${requireContext().packageName}.can_1170",
"${requireContext().packageName}.can_1970"
)
val params = QueryProductDetailsParams.newBuilder()
.setProductList(
productList.map {
QueryProductDetailsParams.Product.newBuilder()
.setProductId(it)
.setProductType(BillingClient.ProductType.INAPP)
.build()
}
)
.build()
billingClient.queryProductDetailsAsync(params) { billingResult, productDetailsList ->
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
handler.post { adapter.addItems(productDetailsList) }
} else {
viewModel.showToast("인 앱 결제 이용이 불가능 합니다. 다시 시도해 주세요.")
}
viewModel.setLoading(false)
}
}
private fun queryAndConsumeUnconsumedPurchases() {
val queryPurchaseParams = QueryPurchasesParams.newBuilder()
.setProductType(BillingClient.ProductType.INAPP)
.build()
billingClient.queryPurchasesAsync(
queryPurchaseParams
) { result, purchaseList ->
if (result.responseCode == BillingClient.BillingResponseCode.OK) {
if (purchaseList.isNotEmpty()) {
for (purchase in purchaseList) {
if (!purchase.isAcknowledged) {
consumePurchase(purchase)
}
}
} else {
queryAvailableCans()
}
}
}
}
private fun consumePurchase(purchase: Purchase) {
val params = ConsumeParams.newBuilder()
.setPurchaseToken(purchase.purchaseToken)
.build()
billingClient.consumeAsync(params) { billingResult, _ ->
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
queryAvailableCans()
}
}
}
private fun handlePurchase(purchase: Purchase) {
if (
purchase.purchaseState == Purchase.PurchaseState.PURCHASED &&
!purchase.isAcknowledged
) {
viewModel.chargeCan(
title = selectedProductDetails!!.name,
selectedProductDetails = selectedProductDetails!!,
purchase = purchase
) { chargeCan ->
handler.post {
viewModel.showToast("캔이 충전되었습니다")
SharedPreferenceManager.can += chargeCan
if (activity != null) {
if (activity as? CanChargeActivity != null) {
(activity as CanChargeActivity).successIapCharge()
} else {
requireActivity().finish()
}
}
}
}
}
}
private fun launchPurchaseFlow(productDetails: ProductDetails) {
val billingFlowParams = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(
listOf(
BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(productDetails)
.build()
)
).build()
billingClient.launchBillingFlow(requireActivity(), billingFlowParams)
}
}

View File

@ -0,0 +1,82 @@
package kr.co.vividnext.sodalive.mypage.can.charge.iap
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.android.billingclient.api.ProductDetails
import com.android.billingclient.api.Purchase
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.CanRepository
class CanChargeIapViewModel(private val repository: CanRepository) : BaseViewModel() {
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
private var _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean>
get() = _isLoading
fun chargeCan(
title: String,
selectedProductDetails: ProductDetails,
purchase: Purchase,
onSuccess: (Int) -> Unit
) {
val productId = purchase.products.firstOrNull()
if (productId != null) {
_isLoading.value = true
compositeDisposable.add(
repository.googleChargeCan(
request = GoogleChargeRequest(
title = title,
chargeCan = selectedProductDetails.description.toInt(),
price = (
selectedProductDetails.oneTimePurchaseOfferDetails?.priceAmountMicros
?: 0L).toDouble() / 1000000,
currencyCode = selectedProductDetails.oneTimePurchaseOfferDetails?.priceCurrencyCode
?: "KRW",
productId = purchase.products[0],
purchaseToken = purchase.purchaseToken
),
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success) {
onSuccess(selectedProductDetails.description.toInt())
} else {
if (it.message != null) {
_toastLiveData.value = it.message
} else {
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
}
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
}
)
)
} else {
_toastLiveData.value = "구매를 하지 못했습니다.\n고객센터로 문의해 주시기 바랍니다."
}
}
fun showToast(message: String) {
_toastLiveData.value = message
}
fun setLoading(isLoading: Boolean) {
_isLoading.value = isLoading
}
}

View File

@ -0,0 +1,14 @@
package kr.co.vividnext.sodalive.mypage.can.charge.iap
import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.mypage.can.payment.PaymentGateway
data class GoogleChargeRequest(
@SerializedName("title") val title: String,
@SerializedName("chargeCan") val chargeCan: Int,
@SerializedName("price") val price: Double,
@SerializedName("currencyCode") val currencyCode: String,
@SerializedName("productId") val productId: String,
@SerializedName("purchaseToken") val purchaseToken: String,
@SerializedName("paymentGateway") val paymentGateway: PaymentGateway = PaymentGateway.GOOGLE_IAP
)

View File

@ -1,4 +1,4 @@
package kr.co.vividnext.sodalive.mypage.can.charge package kr.co.vividnext.sodalive.mypage.can.charge.pg
import android.content.Context import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
@ -10,9 +10,9 @@ import kr.co.vividnext.sodalive.databinding.ItemCanChargeBinding
import kr.co.vividnext.sodalive.extensions.fontSpan import kr.co.vividnext.sodalive.extensions.fontSpan
import kr.co.vividnext.sodalive.extensions.moneyFormat import kr.co.vividnext.sodalive.extensions.moneyFormat
class CanChargeAdapter( class CanChargePgAdapter(
private val onClick: (CanResponse) -> Unit private val onClick: (CanResponse) -> Unit
) : RecyclerView.Adapter<CanChargeAdapter.ViewHolder>() { ) : RecyclerView.Adapter<CanChargePgAdapter.ViewHolder>() {
val items = mutableListOf<CanResponse>() val items = mutableListOf<CanResponse>()

View File

@ -0,0 +1,101 @@
package kr.co.vividnext.sodalive.mypage.can.charge.pg
import android.annotation.SuppressLint
import android.graphics.Rect
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.FragmentCanChargePgBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeActivity
import org.koin.android.ext.android.inject
class CanChargePgFragment : BaseFragment<FragmentCanChargePgBinding>(
FragmentCanChargePgBinding::inflate
) {
private val viewModel: CanChargePgViewModel by inject()
private lateinit var adapter: CanChargePgAdapter
private lateinit var loadingDialog: LoadingDialog
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupView()
bindData()
viewModel.getCanCharges()
}
fun setupView() {
loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
val recyclerView = binding.rvChargeCan
adapter = CanChargePgAdapter {
(requireActivity() as CanChargeActivity).selectCan(it)
}
recyclerView.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.VERTICAL,
false
)
recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
outRect.left = 13.3f.dpToPx().toInt()
outRect.right = 13.3f.dpToPx().toInt()
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.top = 13.3f.dpToPx().toInt()
outRect.bottom = 6.7f.dpToPx().toInt()
}
adapter.itemCount - 1 -> {
outRect.top = 6.7f.dpToPx().toInt()
outRect.bottom = 26.7f.dpToPx().toInt()
}
else -> {
outRect.top = 6.7f.dpToPx().toInt()
outRect.bottom = 6.7f.dpToPx().toInt()
}
}
}
})
recyclerView.adapter = adapter
}
@SuppressLint("NotifyDataSetChanged")
private fun bindData() {
viewModel.canChargeLiveData.observe(viewLifecycleOwner) {
adapter.items.addAll(it)
adapter.notifyDataSetChanged()
}
viewModel.toastLiveData.observe(viewLifecycleOwner) {
it?.let { Toast.makeText(requireActivity(), it, Toast.LENGTH_LONG).show() }
}
viewModel.isLoading.observe(viewLifecycleOwner) {
if (it) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
}
}
}
}

View File

@ -1,4 +1,4 @@
package kr.co.vividnext.sodalive.mypage.can.charge package kr.co.vividnext.sodalive.mypage.can.charge.pg
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
@ -9,7 +9,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.mypage.can.CanRepository import kr.co.vividnext.sodalive.mypage.can.CanRepository
class CanChargeViewModel(private val repository: CanRepository) : BaseViewModel() { class CanChargePgViewModel(private val repository: CanRepository) : BaseViewModel() {
private val _canChargesLiveData = MutableLiveData<List<CanResponse>>() private val _canChargesLiveData = MutableLiveData<List<CanResponse>>()
val canChargeLiveData: LiveData<List<CanResponse>> val canChargeLiveData: LiveData<List<CanResponse>>
get() = _canChargesLiveData get() = _canChargesLiveData

View File

@ -1,4 +1,4 @@
package kr.co.vividnext.sodalive.mypage.can.charge package kr.co.vividnext.sodalive.mypage.can.charge.pg
import android.os.Parcelable import android.os.Parcelable
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName

View File

@ -1,4 +1,4 @@
package kr.co.vividnext.sodalive.mypage.can.charge package kr.co.vividnext.sodalive.mypage.can.charge.pg
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.mypage.can.payment.PaymentGateway import kr.co.vividnext.sodalive.mypage.can.payment.PaymentGateway

View File

@ -23,8 +23,8 @@ import kr.co.vividnext.sodalive.databinding.ActivityCanPaymentBinding
import kr.co.vividnext.sodalive.extensions.fontSpan import kr.co.vividnext.sodalive.extensions.fontSpan
import kr.co.vividnext.sodalive.extensions.moneyFormat import kr.co.vividnext.sodalive.extensions.moneyFormat
import kr.co.vividnext.sodalive.mypage.auth.BootpayResponse import kr.co.vividnext.sodalive.mypage.auth.BootpayResponse
import kr.co.vividnext.sodalive.mypage.can.charge.CanResponse import kr.co.vividnext.sodalive.mypage.can.charge.pg.CanResponse
import kr.co.vividnext.sodalive.mypage.can.charge.VerifyRequest import kr.co.vividnext.sodalive.mypage.can.charge.pg.VerifyRequest
import kr.co.vividnext.sodalive.mypage.can.status.CanStatusActivity import kr.co.vividnext.sodalive.mypage.can.status.CanStatusActivity
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
@ -140,8 +140,8 @@ class CanPaymentActivity : BaseActivity<ActivityCanPaymentBinding>(
R.font.gmarket_sans_bold R.font.gmarket_sans_bold
) )
view.setTextColor(ContextCompat.getColor(applicationContext, R.color.color_9970ff)) view.setTextColor(ContextCompat.getColor(applicationContext, R.color.color_3bb9f1))
view.setBackgroundResource(R.drawable.bg_round_corner_10_4d9970ff_9970ff) view.setBackgroundResource(R.drawable.bg_round_corner_10_13181b_3bb9f1)
} }
private fun requestCharge() { private fun requestCharge() {
@ -211,15 +211,13 @@ class CanPaymentActivity : BaseActivity<ActivityCanPaymentBinding>(
request, request,
onSuccess = { onSuccess = {
Toast.makeText(applicationContext, "캔이 충전되었습니다", Toast.LENGTH_LONG).show() Toast.makeText(applicationContext, "캔이 충전되었습니다", Toast.LENGTH_LONG).show()
SharedPreferenceManager.can += (canResponse!!.rewardCan + canResponse!!.can)
if (gotoPrevPage) { if (gotoPrevPage) {
setResult(RESULT_OK) setResult(RESULT_OK)
} else { } else {
val intent = Intent(applicationContext, CanStatusActivity::class.java) val intent = Intent(applicationContext, CanStatusActivity::class.java)
startActivity(intent) startActivity(intent)
} }
SharedPreferenceManager.can += (canResponse!!.rewardCan + canResponse!!.can)
finish() finish()
}, },
onFailure = { onFailure = {

View File

@ -8,8 +8,8 @@ import io.reactivex.rxjava3.schedulers.Schedulers
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
import kr.co.vividnext.sodalive.mypage.can.CanRepository import kr.co.vividnext.sodalive.mypage.can.CanRepository
import kr.co.vividnext.sodalive.mypage.can.charge.ChargeRequest import kr.co.vividnext.sodalive.mypage.can.charge.pg.ChargeRequest
import kr.co.vividnext.sodalive.mypage.can.charge.VerifyRequest import kr.co.vividnext.sodalive.mypage.can.charge.pg.VerifyRequest
class CanPaymentViewModel(private val repository: CanRepository) : BaseViewModel() { class CanPaymentViewModel(private val repository: CanRepository) : BaseViewModel() {

View File

@ -31,8 +31,6 @@ class OnBoardingActivity : BaseActivity<ActivityOnboardingBinding>(
adapter.addFragment(OnBoardingFragment(R.drawable.img_guide_2)) adapter.addFragment(OnBoardingFragment(R.drawable.img_guide_2))
adapter.addFragment(OnBoardingFragment(R.drawable.img_guide_3)) adapter.addFragment(OnBoardingFragment(R.drawable.img_guide_3))
adapter.addFragment(OnBoardingFragment(R.drawable.img_guide_4)) adapter.addFragment(OnBoardingFragment(R.drawable.img_guide_4))
adapter.addFragment(OnBoardingFragment(R.drawable.img_guide_5))
adapter.addFragment(OnBoardingFragment(R.drawable.img_guide_6))
binding.viewPager.adapter = adapter binding.viewPager.adapter = adapter
} }

View File

@ -6,6 +6,7 @@ import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import com.google.firebase.dynamiclinks.PendingDynamicLinkData import com.google.firebase.dynamiclinks.PendingDynamicLinkData
import com.google.firebase.dynamiclinks.ktx.dynamicLinks import com.google.firebase.dynamiclinks.ktx.dynamicLinks
@ -32,6 +33,9 @@ class SplashActivity : BaseActivity<ActivitySplashBinding>(ActivitySplashBinding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val lp = binding.ivText.layoutParams as ConstraintLayout.LayoutParams
lp.topMargin = screenHeight * 787 / 2337
binding.ivText.layoutParams = lp
setupRemoteConfig() setupRemoteConfig()
fetchAndroidLatestVersion() fetchAndroidLatestVersion()
} }

View File

@ -4,6 +4,7 @@ import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.explorer.profile.MemberBlockRequest import kr.co.vividnext.sodalive.explorer.profile.MemberBlockRequest
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser
import kr.co.vividnext.sodalive.main.GaidUpdateRequest
import kr.co.vividnext.sodalive.main.PushTokenUpdateRequest import kr.co.vividnext.sodalive.main.PushTokenUpdateRequest
import kr.co.vividnext.sodalive.mypage.MyPageResponse import kr.co.vividnext.sodalive.mypage.MyPageResponse
import kr.co.vividnext.sodalive.mypage.profile.ProfileResponse import kr.co.vividnext.sodalive.mypage.profile.ProfileResponse
@ -137,4 +138,10 @@ interface UserApi {
@Part multipartFile: MultipartBody.Part, @Part multipartFile: MultipartBody.Part,
@Header("Authorization") authHeader: String @Header("Authorization") authHeader: String
): Single<ApiResponse<String>> ): Single<ApiResponse<String>>
@PUT("/member/adid/update")
fun updateGaid(
@Body request: GaidUpdateRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
} }

View File

@ -4,6 +4,7 @@ import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.explorer.profile.MemberBlockRequest import kr.co.vividnext.sodalive.explorer.profile.MemberBlockRequest
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser
import kr.co.vividnext.sodalive.main.GaidUpdateRequest
import kr.co.vividnext.sodalive.main.PushTokenUpdateRequest import kr.co.vividnext.sodalive.main.PushTokenUpdateRequest
import kr.co.vividnext.sodalive.mypage.MyPageResponse import kr.co.vividnext.sodalive.mypage.MyPageResponse
import kr.co.vividnext.sodalive.mypage.profile.ProfileResponse import kr.co.vividnext.sodalive.mypage.profile.ProfileResponse
@ -108,4 +109,11 @@ class UserRepository(private val userApi: UserApi) {
fun getProfile(token: String): Single<ApiResponse<ProfileResponse>> { fun getProfile(token: String): Single<ApiResponse<ProfileResponse>> {
return userApi.getMyProfile(authHeader = token) return userApi.getMyProfile(authHeader = token)
} }
fun updateGaid(
request: GaidUpdateRequest,
token: String
): Single<ApiResponse<Any>> {
return userApi.updateGaid(request, authHeader = token)
}
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 563 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 824 B

After

Width:  |  Height:  |  Size: 825 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 775 KiB

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 560 KiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 320 KiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 652 KiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 699 KiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 554 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

View File

@ -0,0 +1,4 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/color_b3333333" />
</shape>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -75,7 +75,7 @@
android:layout_alignParentEnd="true" android:layout_alignParentEnd="true"
android:contentDescription="@null" android:contentDescription="@null"
android:visibility="gone" android:visibility="gone"
tools:src="@drawable/btn_notification_selected" /> tools:src="@drawable/btn_following_big" />
</RelativeLayout> </RelativeLayout>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
@ -122,13 +122,13 @@
android:id="@+id/tv_preview_no" android:id="@+id/tv_preview_no"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:background="@drawable/bg_round_corner_46_7_333333" android:background="@drawable/bg_round_corner_46_7_333333"
android:padding="13.3dp" android:padding="13.3dp"
android:visibility="gone"
android:layout_centerInParent="true"
android:text="해당 콘텐츠는 크리에이터의 요청으로\n미리듣기를 제공하지 않습니다" android:text="해당 콘텐츠는 크리에이터의 요청으로\n미리듣기를 제공하지 않습니다"
android:textColor="@color/color_eeeeee" android:textColor="@color/color_eeeeee"
android:textSize="16.7sp" /> android:textSize="16.7sp"
android:visibility="gone" />
<SeekBar <SeekBar
android:id="@+id/sb_progress" android:id="@+id/sb_progress"
@ -140,6 +140,24 @@
android:paddingEnd="0dp" android:paddingEnd="0dp"
android:progressDrawable="@drawable/audio_content_player_seekbar" android:progressDrawable="@drawable/audio_content_player_seekbar"
android:thumb="@null" /> android:thumb="@null" />
<FrameLayout
android:id="@+id/fl_sold_out"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/color_99000000"
android:visibility="gone" />
<TextView
android:id="@+id/tv_sold_out_big"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:fontFamily="@font/gmarket_sans_bold"
android:text="Sold Out"
android:textColor="@color/white"
android:textSize="36.7sp"
android:visibility="gone" />
</RelativeLayout> </RelativeLayout>
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
@ -364,6 +382,75 @@
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>
<RelativeLayout
android:id="@+id/rl_limited_edition"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="13.3dp"
android:layout_marginTop="13.3dp"
android:background="@drawable/bg_round_corner_5_3_14262d"
android:paddingHorizontal="10.3dp"
android:paddingVertical="8dp"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/gmarket_sans_medium"
android:text="한정판"
android:textColor="@color/color_3bb9f1"
android:textSize="13.3sp" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:orientation="horizontal"
tools:ignore="RelativeOverlap">
<TextView
android:id="@+id/tv_sold_out_small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_round_corner_2_6_transparent_d2d2d2"
android:fontFamily="@font/gmarket_sans_medium"
android:paddingHorizontal="5.3dp"
android:paddingVertical="3.3dp"
android:text="Sold Out"
android:textColor="@color/color_d2d2d2"
android:textSize="12sp"
android:visibility="gone" />
<TextView
android:id="@+id/tv_remaining"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/gmarket_sans_medium"
android:text="잔여수량"
android:textColor="@color/color_d2d2d2"
android:textSize="13.3sp" />
<TextView
android:id="@+id/tv_remaining_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5.3dp"
android:fontFamily="@font/gmarket_sans_bold"
android:textColor="@color/color_3bb9f1"
android:textSize="13.3sp"
tools:text="10" />
<TextView
android:id="@+id/tv_total_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/gmarket_sans_medium"
android:textColor="@color/color_d2d2d2"
android:textSize="13.3sp"
tools:text=" / 10" />
</LinearLayout>
</RelativeLayout>
<FrameLayout <FrameLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -422,6 +509,21 @@
</RelativeLayout> </RelativeLayout>
</FrameLayout> </FrameLayout>
<TextView
android:id="@+id/tv_purchase_sold_out"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="13.3dp"
android:layout_marginTop="18.3dp"
android:background="@drawable/bg_round_corner_5_3_525252"
android:fontFamily="@font/gmarket_sans_bold"
android:gravity="center"
android:paddingVertical="16dp"
android:text="해당 콘텐츠가 매진되었습니다."
android:textColor="@color/white"
android:textSize="13.3sp"
android:visibility="gone" />
<RelativeLayout <RelativeLayout
android:id="@+id/ll_purchase" android:id="@+id/ll_purchase"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -579,6 +681,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="11dp" android:layout_marginStart="11dp"
android:ellipsize="end" android:ellipsize="end"
android:lineSpacingExtra="8dp"
android:maxLines="1" android:maxLines="1"
android:textColor="@color/color_bbbbbb" android:textColor="@color/color_bbbbbb"
android:textSize="12sp" android:textSize="12sp"

View File

@ -1,20 +1,29 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"> android:orientation="vertical">
<include <include
android:id="@+id/toolbar" android:id="@+id/toolbar"
layout="@layout/detail_toolbar" /> layout="@layout/detail_toolbar" />
<androidx.recyclerview.widget.RecyclerView <com.google.android.material.tabs.TabLayout
android:id="@+id/rv_charge_can" android:id="@+id/tabs"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="50dp"
app:layout_constraintBottom_toBottomOf="parent" android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent" app:tabIndicatorColor="@color/color_80d8ff"
app:layout_constraintStart_toStartOf="parent" app:tabIndicatorFullWidth="true"
app:layout_constraintTop_toBottomOf="@+id/toolbar" /> app:tabIndicatorHeight="1.3dp"
app:tabSelectedTextColor="@color/color_eeeeee"
app:tabTextAppearance="@style/tabText"
app:tabTextColor="@color/color_777777" />
</androidx.constraintlayout.widget.ConstraintLayout> <FrameLayout
android:id="@+id/fl_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

View File

@ -2,6 +2,7 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@color/black"
android:orientation="vertical"> android:orientation="vertical">
<include <include

View File

@ -11,12 +11,12 @@
android:id="@+id/iv_cover" android:id="@+id/iv_cover"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_marginTop="16dp"
android:contentDescription="@null" android:contentDescription="@null"
android:scaleType="fitCenter" android:scaleType="fitCenter"
android:layout_marginTop="16dp" app:layout_constraintBottom_toTopOf="@+id/rl_input_chat"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="@+id/rl_input_chat"
app:layout_constraintTop_toBottomOf="@id/ll_top" /> app:layout_constraintTop_toBottomOf="@id/ll_top" />
<View <View
@ -104,6 +104,59 @@
</ScrollView> </ScrollView>
</LinearLayout> </LinearLayout>
<LinearLayout
android:id="@+id/ll_menu_pan"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginHorizontal="60dp"
android:layout_marginBottom="13.3dp"
android:orientation="vertical"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@+id/rl_input_chat"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ll_top">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/ic_notice_triangle" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="none">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/color_333333"
android:orientation="vertical"
android:padding="8dp">
<TextView
android:id="@+id/tv_menu_pan_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/gmarket_sans_bold"
android:text="[메뉴판] "
android:textColor="@color/white"
android:textSize="11.3sp" />
<TextView
android:id="@+id/tv_menu_pan_detail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/gmarket_sans_light"
android:lineSpacingExtra="4dp"
android:textColor="@color/white"
android:textSize="11.3sp"
tools:text="jkljkljkljkljkljkljkl" />
</LinearLayout>
</ScrollView>
</LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/ll_top" android:id="@+id/ll_top"
android:layout_width="0dp" android:layout_width="0dp"
@ -143,6 +196,22 @@
android:gravity="end" android:gravity="end"
android:orientation="horizontal"> android:orientation="horizontal">
<TextView
android:id="@+id/tv_change_listener"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:background="@drawable/bg_round_corner_5_3_transparent_bbbbbb"
android:fontFamily="@font/gmarket_sans_medium"
android:gravity="center"
android:paddingHorizontal="8dp"
android:paddingVertical="4.7dp"
android:text="리스너 변경"
android:textColor="@color/color_eeeeee"
android:textSize="12sp"
android:visibility="gone"
tools:ignore="SmallSp" />
<TextView <TextView
android:id="@+id/tv_bg_switch" android:id="@+id/tv_bg_switch"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -225,9 +294,9 @@
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:layout_marginHorizontal="5.3dp" android:layout_marginHorizontal="5.3dp"
android:layout_toStartOf="@+id/iv_creator_follow"
android:layout_toEndOf="@+id/rl_creator_profile" android:layout_toEndOf="@+id/rl_creator_profile"
android:orientation="vertical"> android:orientation="vertical">
@ -262,27 +331,33 @@
tools:text="오늘 라이브 방송은ㅇㄹ너ㅏㅣㅇㄴ럴ㄴ아ㅣㄴㅇ러ㅏㅣ" /> tools:text="오늘 라이브 방송은ㅇㄹ너ㅏㅣㅇㄴ럴ㄴ아ㅣㄴㅇ러ㅏㅣ" />
</LinearLayout> </LinearLayout>
<TextView <LinearLayout
android:id="@+id/tv_creator_nickname"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="2.7dp" android:gravity="center_vertical"
android:ellipsize="end" android:orientation="horizontal">
android:fontFamily="@font/gmarket_sans_medium"
android:lines="1"
android:textColor="@color/color_777777"
android:textSize="12sp"
tools:text="청령" />
</LinearLayout>
<ImageView <TextView
android:id="@+id/iv_creator_follow" android:id="@+id/tv_creator_nickname"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentEnd="true" android:layout_marginTop="2.7dp"
android:layout_centerVertical="true" android:ellipsize="end"
android:contentDescription="@null" android:fontFamily="@font/gmarket_sans_medium"
android:src="@drawable/btn_plus_round" /> android:lines="1"
android:textColor="@color/color_777777"
android:textSize="12sp"
tools:text="청령" />
<ImageView
android:id="@+id/iv_creator_follow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5.3dp"
android:contentDescription="@null"
android:src="@drawable/btn_follow" />
</LinearLayout>
</LinearLayout>
</RelativeLayout> </RelativeLayout>
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
@ -308,7 +383,6 @@
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:background="@drawable/bg_round_corner_5_3_transparent_bbbbbb" android:background="@drawable/bg_round_corner_5_3_transparent_bbbbbb"
android:drawablePadding="2.7dp"
android:fontFamily="@font/gmarket_sans_medium" android:fontFamily="@font/gmarket_sans_medium"
android:paddingHorizontal="8dp" android:paddingHorizontal="8dp"
android:paddingVertical="5.3dp" android:paddingVertical="5.3dp"
@ -317,6 +391,23 @@
android:textSize="12sp" android:textSize="12sp"
tools:ignore="SmallSp" /> tools:ignore="SmallSp" />
<TextView
android:id="@+id/tv_menu_pan"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginStart="5.3dp"
android:layout_toEndOf="@+id/tv_notification"
android:background="@drawable/bg_round_corner_5_3_transparent_bbbbbb"
android:fontFamily="@font/gmarket_sans_medium"
android:paddingHorizontal="8dp"
android:paddingVertical="5.3dp"
android:text="메뉴판"
android:textColor="@color/color_bbbbbb"
android:textSize="12sp"
android:visibility="gone"
tools:ignore="SmallSp" />
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -357,7 +448,7 @@
android:id="@+id/ll_view_users" android:id="@+id/ll_view_users"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginStart="5.3dp"
android:background="@drawable/bg_round_corner_5_3_transparent_bbbbbb" android:background="@drawable/bg_round_corner_5_3_transparent_bbbbbb"
android:orientation="horizontal" android:orientation="horizontal"
android:paddingHorizontal="11dp" android:paddingHorizontal="11dp"
@ -571,4 +662,16 @@
android:contentDescription="@null" android:contentDescription="@null"
android:src="@drawable/btn_message_send" /> android:src="@drawable/btn_message_send" />
</RelativeLayout> </RelativeLayout>
<ImageView
android:id="@+id/iv_signature"
android:layout_width="0dp"
android:layout_height="200dp"
android:layout_marginHorizontal="20dp"
android:layout_marginBottom="65dp"
android:contentDescription="@null"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

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