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"
minSdk 23
targetSdk 33
versionCode 22
versionName "1.6.0"
versionCode 46
versionName "1.8.21"
}
buildTypes {
@ -149,4 +149,7 @@ dependencies {
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.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"
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.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.INTERNET" />
@ -84,7 +85,9 @@
<activity android:name=".settings.terms.TermsActivity" />
<activity android:name=".user.find_password.FindPasswordActivity" />
<activity android:name=".mypage.can.status.CanStatusActivity" />
<activity android:name=".mypage.can.charge.CanChargeActivity" />
<activity
android:name=".mypage.can.charge.CanChargeActivity"
android:configChanges="orientation|screenSize|keyboardHidden" />
<activity android:name=".mypage.can.payment.CanPaymentActivity" />
<activity android:name=".mypage.can.coupon.CanCouponActivity" />
<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.base.BaseActivity
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.GridSpacingItemDecoration
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentNewAllBinding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
@ -89,8 +90,10 @@ class AudioContentNewAllActivity : BaseActivity<ActivityAudioContentNewAllBindin
}
private fun setupNewContent() {
val spanCount = 3
val spacing = 40
newContentAdapter = AudioContentNewAllAdapter(
itemWidth = (screenWidth - 42f.dpToPx().toInt()) / 3,
itemWidth = (screenWidth - (spacing * (spanCount + 1))) / 3,
onClickItem = {
startActivity(
Intent(this, AudioContentDetailActivity::class.java).apply {
@ -107,23 +110,8 @@ class AudioContentNewAllActivity : BaseActivity<ActivityAudioContentNewAllBindin
}
)
binding.rvContent.layoutManager = GridLayoutManager(this, 3)
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.layoutManager = GridLayoutManager(this, spanCount)
binding.rvContent.addItemDecoration(GridSpacingItemDecoration(spanCount, spacing, true))
binding.rvContent.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {

View File

@ -1,6 +1,9 @@
package kr.co.vividnext.sodalive.audio_content.all
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -8,8 +11,12 @@ import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
import coil.transform.RoundedCornersTransformation
import com.orhanobut.logger.Logger
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.CenterCrop
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem
import kr.co.vividnext.sodalive.databinding.ItemAudioContentNewAllBinding
@ -23,24 +30,27 @@ class AudioContentNewAllAdapter(
) : RecyclerView.Adapter<AudioContentNewAllAdapter.ViewHolder>() {
inner class ViewHolder(
private val context: Context,
private val binding: ItemAudioContentNewAllBinding,
private val onClickItem: (Long) -> Unit,
private val onClickCreator: (Long) -> Unit
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetAudioContentMainItem) {
Logger.e("item: $item")
binding.ivAudioContentCoverImage.load(item.coverImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(2.7f.dpToPx()))
Glide
.with(context)
.load(item.coverImageUrl)
.apply(
RequestOptions().transform(
CenterCrop(),
RoundedCorners(8)
)
)
.into(binding.ivAudioContentCoverImage)
val layoutParams = binding.ivAudioContentCoverImage
.layoutParams as ConstraintLayout.LayoutParams
layoutParams.width = itemWidth
layoutParams.height = itemWidth
binding.ivAudioContentCoverImage.layoutParams = layoutParams
}
val layoutParams = binding.ivAudioContentCoverImage.layoutParams as ConstraintLayout.LayoutParams
layoutParams.width = itemWidth
layoutParams.height = itemWidth
binding.ivAudioContentCoverImage.layoutParams = layoutParams
binding.ivAudioContentCreator.load(item.creatorProfileImageUrl) {
crossfade(true)
@ -72,6 +82,7 @@ class AudioContentNewAllAdapter(
parent: ViewGroup,
viewType: Int
) = ViewHolder(
parent.context,
ItemAudioContentNewAllBinding.inflate(
LayoutInflater.from(parent.context),
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.base.BaseActivity
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.GridSpacingItemDecoration
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentAllByThemeBinding
import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity
@ -55,8 +56,10 @@ class AudioContentAllByThemeActivity : BaseActivity<ActivityAudioContentAllByThe
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.setOnClickListener { finish() }
val spanCount = 3
val spacing = 40
adapter = AudioContentNewAllAdapter(
itemWidth = (screenWidth - 42f.dpToPx().toInt()) / 3,
itemWidth = (screenWidth - (spacing * (spanCount + 1))) / 3,
onClickItem = {
startActivity(
Intent(this, AudioContentDetailActivity::class.java).apply {
@ -73,23 +76,8 @@ class AudioContentAllByThemeActivity : BaseActivity<ActivityAudioContentAllByThe
}
)
binding.rvContentAll.layoutManager = GridLayoutManager(this, 3)
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.layoutManager = GridLayoutManager(this, spanCount)
binding.rvContentAll.addItemDecoration(GridSpacingItemDecoration(spanCount, spacing, true))
binding.rvContentAll.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {

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

View File

@ -238,10 +238,16 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
}
})
val layoutParams = binding.ivCover.layoutParams as RelativeLayout.LayoutParams
layoutParams.width = (screenWidth - 13.3f.dpToPx()).toInt()
layoutParams.height = (screenWidth - 13.3f.dpToPx()).toInt()
binding.ivCover.layoutParams = layoutParams
val ivCoverLp = binding.ivCover.layoutParams as RelativeLayout.LayoutParams
ivCoverLp.width = (screenWidth - 13.3f.dpToPx()).toInt()
ivCoverLp.height = (screenWidth - 13.3f.dpToPx()).toInt()
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.llDonation.setOnClickListener {
val dialog = LiveRoomDonationDialog(
@ -481,6 +487,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
if (response.releaseDate != null) {
binding.llPurchase.visibility = View.VISIBLE
binding.llPurchasePrice.visibility = View.GONE
binding.tvPurchaseSoldOut.visibility = View.GONE
binding.tvReleaseDate.visibility = View.VISIBLE
binding.llPurchase.background = ContextCompat.getDrawable(
applicationContext,
@ -494,26 +501,50 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
response.orderType == null &&
response.creator.creatorId != SharedPreferenceManager.userId
) {
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.tvStrPurchaseOrRental.text = if (response.isOnlyRental) {
" 대여하기"
if (
response.totalContentCount != null && response.remainingContentCount != null &&
response.remainingContentCount <= 0
) {
binding.llPurchase.visibility = View.GONE
binding.tvPurchaseSoldOut.visibility = View.VISIBLE
} 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 {
showOrderDialog(audioContent = response, isOnlyRental = response.isOnlyRental)
binding.tvStrPurchaseOrRental.text = if (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 {
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()))
.into(binding.ivCover)
binding.flSoldOut.visibility = View.GONE
binding.tvSoldOutBig.visibility = View.GONE
binding.ivPlayOrPause.visibility = View.GONE
binding.tvPreviewNo.visibility = View.GONE
binding.tvTotalDuration.text = " / ${response.duration}"
@ -536,9 +569,16 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
response.price > 0
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 &&
!isAlertPreview ||
(isAlertPreview && response.isActivePreview)
(response.isActivePreview && response.contentUrl.isNotBlank())
) {
binding.ivPlayOrPause.visibility = View.VISIBLE
binding.ivPlayOrPause.setOnClickListener {
@ -572,7 +612,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
R.drawable.btn_audio_content_preview_play
}
)
} else {
} else if (response.releaseDate == null) {
binding.tvPreviewNo.visibility = View.VISIBLE
}
}
@ -657,6 +697,33 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
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) {
@ -702,7 +769,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
binding.ivFollow.visibility = View.VISIBLE
if (creator.isFollowing) {
binding.ivFollow.setImageResource(R.drawable.btn_notification_selected)
binding.ivFollow.setImageResource(R.drawable.btn_following_big)
binding.ivFollow.setOnClickListener {
viewModel.unRegisterNotification(
contentId = audioContentId,
@ -710,7 +777,7 @@ class AudioContentDetailActivity : BaseActivity<ActivityAudioContentDetailBindin
)
}
} else {
binding.ivFollow.setImageResource(R.drawable.btn_notification)
binding.ivFollow.setImageResource(R.drawable.btn_follow_big)
binding.ivFollow.setOnClickListener {
viewModel.registerNotification(
contentId = audioContentId,

View File

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

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.detail.LiveRoomDetailViewModel
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.LiveTagViewModel
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.can.CanApi
import kr.co.vividnext.sodalive.mypage.can.CanRepository
import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeViewModel
import kr.co.vividnext.sodalive.mypage.can.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.payment.CanPaymentViewModel
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(), AuthApi::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(), TermsApi::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 { MyPageViewModel(get(), get()) }
viewModel { CanStatusViewModel(get()) }
viewModel { CanChargeViewModel(get()) }
viewModel { CanChargePgViewModel(get()) }
viewModel { CanPaymentViewModel(get()) }
viewModel { LiveRoomDetailViewModel(get()) }
viewModel { LiveRoomCreateViewModel(get()) }
@ -229,12 +232,13 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
viewModel { CreatorCommunityWriteViewModel(get()) }
viewModel { CreatorCommunityModifyViewModel(get()) }
viewModel { CanCouponViewModel(get()) }
viewModel { CanChargeIapViewModel(get()) }
}
private val repositoryModule = module {
factory { UserRepository(get()) }
factory { TermsRepository(get()) }
factory { LiveRepository(get(), get()) }
factory { LiveRepository(get(), get(), get()) }
factory { EventRepository(get()) }
factory { LiveRecommendRepository(get()) }
factory { AuthRepository(get()) }

View File

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

View File

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

View File

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

View File

@ -160,7 +160,7 @@ interface LiveApi {
fun donation(
@Body request: LiveRoomDonationRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
): Single<ApiResponse<String>>
@POST("/live/room/donation/refund/{id}")
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.LiveRoomDonationRequest
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.UserApi
import okhttp3.MultipartBody
@ -23,7 +24,8 @@ import java.util.TimeZone
class LiveRepository(
private val api: LiveApi,
private val userApi: UserApi
private val userApi: UserApi,
private val menuApi: MenuApi
) {
fun roomList(
dateString: String? = null,
@ -161,7 +163,7 @@ class LiveRepository(
can: Int,
message: String,
token: String
): Single<ApiResponse<Any>> {
): Single<ApiResponse<String>> {
return api.donation(
request = LiveRoomDonationRequest(
roomId = roomId,
@ -230,4 +232,9 @@ class LiveRepository(
request: CancelLiveReservationRequest,
token: String
) = 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
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.RecyclerView
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.databinding.ItemLiveNowBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
@ -20,15 +29,28 @@ class LiveNowAdapter(
var items = mutableListOf<GetRoomListResponse>()
inner class ViewHolder(
private val context: Context,
private val binding: ItemLiveNowBinding
) : RecyclerView.ViewHolder(binding.root) {
@SuppressLint("SetTextI18n")
fun bind(item: GetRoomListResponse) {
binding.ivCover.loadUrl(item.coverImageUrl) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(RoundedCornersTransformation(4.7f.dpToPx()))
}
Glide
.with(context)
.load(item.coverImageUrl)
.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) {
View.VISIBLE
} else {
@ -36,11 +58,18 @@ class LiveNowAdapter(
}
if (item.price > 0) {
binding.tvPrice.text = "유료"
binding.tvPrice.setBackgroundResource(R.drawable.bg_round_corner_10_881609)
binding.tvPrice.text = "${item.price}"
binding.tvPrice.setCompoundDrawablesWithIntrinsicBounds(
R.drawable.ic_can_white,
0,
0,
0
)
binding.tvPrice.setBackgroundResource(R.drawable.bg_round_corner_13_3_dd4500)
} else {
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()) {
@ -63,6 +92,7 @@ class LiveNowAdapter(
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
parent.context,
ItemLiveNowBinding.inflate(
LayoutInflater.from(parent.context),
parent,

View File

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

View File

@ -2,20 +2,29 @@ package kr.co.vividnext.sodalive.live.now.all
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
import coil.transform.RoundedCornersTransformation
import com.bumptech.glide.Glide
import com.bumptech.glide.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.databinding.ItemLiveNowAllBinding
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
class LiveNowAllAdapter(
private val itemWidth: Int,
private val onClick: (GetRoomListResponse) -> Unit
) : RecyclerView.Adapter<LiveNowAllAdapter.ViewHolder>() {
@ -27,48 +36,72 @@ class LiveNowAllAdapter(
) : RecyclerView.ViewHolder(binding.root) {
@SuppressLint("SetTextI18n")
fun bind(item: GetRoomListResponse) {
binding.ivCover.load(item.coverImageUrl) {
crossfade(true)
placeholder(R.drawable.bg_placeholder)
transformations(RoundedCornersTransformation(4.7f.dpToPx()))
}
binding.tvNickname.text = item.creatorNickname
binding.tvTitle.text = item.title
Glide
.with(context)
.load(item.coverImageUrl)
.apply(
RequestOptions().transform(
CenterCrop(),
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) {
View.VISIBLE
} else {
View.GONE
}
if (item.numberOfPeople > item.numberOfParticipate) {
binding.tvAvailableParticipate.text = "참여가능"
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()
if (item.price > 0) {
binding.tvPrice.text = "${item.price}"
binding.tvPrice.setCompoundDrawablesWithIntrinsicBounds(
R.drawable.ic_can_white,
0,
0,
R.drawable.ic_can,
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) }

View File

@ -26,12 +26,11 @@ import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.widget.PopupMenu
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
import com.bumptech.glide.Glide
import com.github.dhaval2404.imagepicker.ImagePicker
import com.google.gson.Gson
import com.orhanobut.logger.Logger
@ -115,6 +114,18 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
private var isNoChatting = false
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) {
override fun onTick(millisUntilFinished: Long) {
remainingNoChattingTime -= 1
@ -454,6 +465,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
dialog.show(screenWidth)
}
binding.tvNotification.setOnClickListener { viewModel.toggleShowNotice() }
binding.tvMenuPan.setOnClickListener { viewModel.toggleShowMenuPan() }
binding.tvBgSwitch.setOnClickListener { viewModel.toggleBackgroundImage() }
binding.llDonation.setOnClickListener {
@ -696,37 +708,44 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
if (response.creatorId == SharedPreferenceManager.userId) {
binding.ivEdit.setOnClickListener {
roomInfoEditDialog.setRoomInfo(response.title, response.notice)
roomInfoEditDialog.setCoverImageUrl(response.coverImageUrl)
roomInfoEditDialog.setConfirmAction { newTitle, newContent, newCoverImageUri ->
viewModel.editLiveRoomInfo(
response.roomId,
newTitle,
newContent,
newCoverImageUri,
onSuccess = {
binding.tvTitle.text = newTitle
setNoticeAndClickableUrl(binding.tvNotice, newContent)
viewModel.getAllMenuPreset {
roomInfoEditDialog.setRoomInfo(response.title, response.notice)
roomInfoEditDialog.setCoverImageUrl(response.coverImageUrl)
roomInfoEditDialog.setMenuPreset(it)
roomInfoEditDialog.setConfirmAction { newTitle, newContent, newCoverImageUri, isActivateMenu, menuId, menu ->
viewModel.editLiveRoomInfo(
response.roomId,
newTitle,
newContent,
newCoverImageUri,
isActivateMenu,
menuId,
menu,
onSuccess = {
Toast.makeText(
applicationContext,
"라이브 정보가 수정되었습니다.",
Toast.LENGTH_LONG
).show()
if (newCoverImageUri != null) {
binding.ivCover.load(newCoverImageUri)
agora.sendRawMessageToGroup(
rawMessage = Gson().toJson(
LiveRoomChatRawMessage(
type = LiveRoomChatRawMessageType.EDIT_ROOM_INFO,
message = "",
can = 0,
donationMessage = ""
)
).toByteArray()
)
}
)
}
agora.sendRawMessageToGroup(
rawMessage = Gson().toJson(
LiveRoomChatRawMessage(
type = LiveRoomChatRawMessageType.EDIT_ROOM_INFO,
message = "",
can = 0,
donationMessage = ""
)
).toByteArray()
)
}
)
handler.post {
roomInfoEditDialog.show(screenWidth)
}
}
roomInfoEditDialog.show(screenWidth)
}
binding.ivEdit.visibility = View.VISIBLE
@ -781,7 +800,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
if (response.creatorId != SharedPreferenceManager.userId) {
binding.ivCreatorFollow.visibility = View.VISIBLE
if (response.isFollowing) {
binding.ivCreatorFollow.setImageResource(R.drawable.btn_select_checked)
binding.ivCreatorFollow.setImageResource(R.drawable.btn_following)
binding.ivCreatorFollow.setOnClickListener {
viewModel.creatorUnFollow(
creatorId = response.creatorId,
@ -789,7 +808,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
)
}
} else {
binding.ivCreatorFollow.setImageResource(R.drawable.btn_plus_round)
binding.ivCreatorFollow.setImageResource(R.drawable.btn_follow)
binding.ivCreatorFollow.setOnClickListener {
viewModel.creatorFollow(
creatorId = response.creatorId,
@ -807,6 +826,15 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
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()) {
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) {
binding.tvTotalCan.text = it.moneyFormat()
}
@ -1026,6 +1082,8 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
agora.muteLocalAudioStream(false)
agora.setClientRole(io.agora.rtc2.Constants.CLIENT_ROLE_AUDIENCE)
handler.postDelayed({
binding.tvChangeListener.visibility = View.GONE
binding.tvChangeListener.setOnClickListener { }
binding.ivMicrophoneMute.setImageResource(R.drawable.ic_mic_on)
binding.flMicrophoneMute.visibility = View.GONE
binding.ivNotiMicrophoneMute.visibility = View.GONE
@ -1211,16 +1269,18 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
private fun donation(can: Int, message: String) {
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(
rawMessage = donationRawMessage.toByteArray(),
onSuccess = {
@ -1240,6 +1300,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
)
invalidateChat()
viewModel.addDonationCan(can)
addSignatureImage(signatureImage)
}
},
onFailure = {
@ -1346,6 +1407,9 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
)
invalidateChat()
viewModel.addDonationCan(rawMessage.can)
addSignatureImage(
imageUrl = rawMessage.signatureImageUrl ?: ""
)
}
}
@ -1622,6 +1686,18 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
showDialog(content = "스피커가 되었어요!")
setBroadcaster()
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 {
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.room.donation.GetLiveRoomDonationStatusResponse
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.update.EditLiveRoomInfoRequest
import kr.co.vividnext.sodalive.live.roulette.RouletteItem
@ -54,6 +55,10 @@ class LiveRoomViewModel(
val isShowNotice: LiveData<Boolean>
get() = _isShowNotice
private val _isShowMenuPan = MutableLiveData(false)
val isShowMenuPan: LiveData<Boolean>
get() = _isShowMenuPan
private val _totalDonationCan = MutableLiveData(0)
val totalDonationCan: LiveData<Int>
get() = _totalDonationCan
@ -355,9 +360,20 @@ class LiveRoomViewModel(
}
fun toggleShowNotice() {
_isShowMenuPan.value = false
_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() {
_isBgOn.value = !isBgOn.value!!
}
@ -367,6 +383,9 @@ class LiveRoomViewModel(
newTitle: String,
newContent: String,
newCoverImageUri: Uri? = null,
isActivateMenu: Boolean?,
menuId: Long,
menu: String,
onSuccess: () -> Unit
) {
val request = EditLiveRoomInfoRequest(
@ -382,17 +401,25 @@ class LiveRoomViewModel(
},
numberOfPeople = 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)
} else {
null
}
val coverImage = if (newCoverImageUri != null) {
val file = File(getRealPathFromURI(newCoverImageUri!!))
val file = File(getRealPathFromURI(newCoverImageUri))
MultipartBody.Part.createFormData(
"coverImage",
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)
compositeDisposable.add(
repository.donation(roomId, can, message, "Bearer ${SharedPreferenceManager.token}")
@ -492,7 +519,7 @@ class LiveRoomViewModel(
_isLoading.value = false
if (it.success) {
SharedPreferenceManager.can -= can
onSuccess()
onSuccess(it.data ?: "")
} else {
if (it.message != null) {
_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(
can: Int,
items: List<RouletteItem>,

View File

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

View File

@ -6,6 +6,7 @@ data class LiveRoomChatRawMessage(
@SerializedName("type") val type: LiveRoomChatRawMessageType,
@SerializedName("message") val message: String,
@SerializedName("can") val can: Int,
@SerializedName("signatureImageUrl") val signatureImageUrl: String? = null,
@SerializedName("donationMessage") val donationMessage: String?,
@SerializedName("isActiveRoulette") val isActiveRoulette: Boolean? = null
)

View File

@ -14,5 +14,8 @@ data class CreateLiveRoomRequest(
@SerializedName("beginDateTimeString") val beginDateTimeString: String? = null,
@SerializedName("timezone") val timezone: String,
@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.view.MotionEvent
import android.view.View
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
@ -127,6 +128,8 @@ class LiveRoomCreateActivity : BaseActivity<ActivityLiveRoomCreateBinding>(
viewModel.setTimeNow(
intent.getBooleanExtra(Constants.EXTRA_LIVE_TIME_NOW, true)
)
viewModel.getAllMenuPreset()
}
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
@ -295,6 +298,30 @@ class LiveRoomCreateActivity : BaseActivity<ActivityLiveRoomCreateBinding>(
}
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")
@ -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) {
it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() }
}
@ -573,6 +609,44 @@ class LiveRoomCreateActivity : BaseActivity<ActivityLiveRoomCreateBinding>(
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
)
}
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.live.LiveRepository
import kr.co.vividnext.sodalive.live.room.LiveRoomType
import kr.co.vividnext.sodalive.live.room.menu.GetMenuPresetResponse
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
@ -22,6 +23,10 @@ class LiveRoomCreateViewModel(
private val repository: LiveRepository
) : BaseViewModel() {
enum class SelectedMenu {
MENU_1, MENU_2, MENU_3
}
private val _roomTypeLiveData = MutableLiveData(LiveRoomType.OPEN)
val roomTypeLiveData: LiveData<LiveRoomType>
get() = _roomTypeLiveData
@ -58,6 +63,18 @@ class LiveRoomCreateViewModel(
val isAdultLiveData: LiveData<Boolean>
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?
var title = ""
@ -70,6 +87,10 @@ class LiveRoomCreateViewModel(
var coverImagePath: String? = null
var password: String? = null
private var menuId = 0L
var menu = ""
val menuList = mutableListOf<GetMenuPresetResponse>()
fun setRoomType(roomType: LiveRoomType) {
if (_roomTypeLiveData.value!! != roomType) {
_roomTypeLiveData.postValue(roomType)
@ -115,7 +136,18 @@ class LiveRoomCreateViewModel(
password
} else {
null
}
},
menuPanId = if (_isActivateMenuLiveData.value!!) {
menuId
} else {
0
},
menuPan = if (_isActivateMenuLiveData.value!!) {
menu
} else {
""
},
isActiveMenuPan = _isActivateMenuLiveData.value!!
)
val requestJson = Gson().toJson(request)
@ -207,6 +239,11 @@ class LiveRoomCreateViewModel(
return false
}
if (_isActivateMenuLiveData.value!! && menu.isBlank()) {
_toastLiveData.postValue("메뉴판은 빈칸일 수 없습니다.")
return false
}
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("managerList") val managerList: List<LiveRoomMember>,
@SerializedName("donationRankingTop3UserIds") val donationRankingTop3UserIds: List<Long>,
@SerializedName("menuPan") val menuPan: String,
@SerializedName("isActiveRoulette") val isActiveRoulette: Boolean,
@SerializedName("isPrivateRoom") val isPrivateRoom: Boolean,
@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("numberOfPeople") val numberOfPeople: Int?,
@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
import android.app.Activity
import android.annotation.SuppressLint
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
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.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.lifecycle.MutableLiveData
import coil.load
import coil.transform.RoundedCornersTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.DialogLiveRoomInfoUpdateBinding
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(
activity: Activity,
private val activity: AppCompatActivity,
layoutInflater: LayoutInflater,
onClickImagePicker: () -> Unit
) {
@ -24,6 +36,17 @@ class LiveRoomInfoEditDialog(
private var coverImageUrl: String? = 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 {
val dialogBuilder = AlertDialog.Builder(activity)
dialogBuilder.setView(dialogView.root)
@ -35,6 +58,77 @@ class LiveRoomInfoEditDialog(
dialogView.ivPhotoPicker.setOnClickListener { onClickImagePicker() }
dialogView.ivClose.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(
@ -42,7 +136,7 @@ class LiveRoomInfoEditDialog(
currentContent: String,
) {
dialogView.etTitle.setText(currentTitle)
dialogView.etContent.setText(currentContent)
dialogView.etNotice.setText(currentContent)
}
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 {
alertDialog.dismiss()
val newTitle = dialogView.etTitle.text.toString()
val newContent = dialogView.etContent.text.toString()
confirmAction(newTitle, newContent, coverImageUri)
val newContent = dialogView.etNotice.text.toString()
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
coverImageUrl = null
}
@ -85,4 +219,103 @@ class LiveRoomInfoEditDialog(
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 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.UpdateRouletteRequest
class RouletteRepository(private val api: RouletteApi) {
fun createOrUpdateRoulette(
request: CreateOrUpdateRouletteRequest,
fun createRoulette(
request: CreateRouletteRequest,
token: String
) = api.createOrUpdateRoulette(
) = api.createRoulette(
request = request,
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(
creatorId = creatorId,
authHeader = token

View File

@ -3,7 +3,7 @@ package kr.co.vividnext.sodalive.live.roulette.config
import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.live.roulette.RouletteItem
data class CreateOrUpdateRouletteRequest(
data class CreateRouletteRequest(
@SerializedName("can") val can: Int,
@SerializedName("isActive") val isActive: Boolean,
@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 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.SpinRouletteRequest
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Path
import retrofit2.http.Query
interface RouletteApi {
@POST("/roulette")
fun createOrUpdateRoulette(
@Body request: CreateOrUpdateRouletteRequest,
@POST("/new-roulette")
fun createRoulette(
@Body request: CreateRouletteRequest,
@Header("Authorization") authHeader: String
): 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(
@Query("creatorId") creatorId: Long,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetRouletteResponse>>
@POST("/roulette/spin")
@POST("/new-roulette/spin")
fun spinRoulette(
@Body request: SpinRouletteRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetRouletteResponse>>
@POST("/roulette/refund/{id}")
@POST("/new-roulette/refund/{id}")
fun refundRouletteDonation(
@Path("id") id: Long,
@Header("Authorization") authHeader: String

View File

@ -13,7 +13,9 @@ import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
import com.jakewharton.rxbinding4.widget.textChanges
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
@ -41,7 +43,7 @@ class RouletteSettingsFragment : BaseFragment<FragmentRouletteSettingsBinding>(
setupView()
bindData()
viewModel.getRoulette()
viewModel.getAllRoulette()
}
private fun setupView() {
@ -74,15 +76,57 @@ class RouletteSettingsFragment : BaseFragment<FragmentRouletteSettingsBinding>(
handler.postDelayed({
imm.hideSoftInputFromWindow(view?.windowToken, 0)
}, 100)
viewModel.createOrUpdateRoulette {
val resultIntent = Intent().apply { putExtra(Constants.EXTRA_RESULT_ROULETTE, it) }
requireActivity().setResult(Activity.RESULT_OK, resultIntent)
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() {
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) {
binding.ivRouletteIsActive.setImageResource(
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
}
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 kr.co.vividnext.sodalive.base.BaseViewModel
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.RoulettePreview
import kr.co.vividnext.sodalive.live.roulette.RoulettePreviewItem
@ -15,6 +16,10 @@ import kotlin.math.floor
class RouletteSettingsViewModel(private val repository: RouletteRepository) : BaseViewModel() {
enum class SelectedRoulette {
ROULETTE_1, ROULETTE_2, ROULETTE_3
}
private var _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean>
get() = _isLoading
@ -39,9 +44,16 @@ class RouletteSettingsViewModel(private val repository: RouletteRepository) : Ba
val roulettePreviewLiveData: LiveData<RoulettePreview>
get() = _roulettePreviewLiveData
private val options = mutableListOf<RouletteOption>()
private val _selectedRouletteLiveData = MutableLiveData<SelectedRoulette>()
val selectedRouletteLiveData: LiveData<SelectedRoulette>
get() = _selectedRouletteLiveData
var can = 0
var isActive = false
private var rouletteId = 0L
private val options = mutableListOf<RouletteOption>()
val rouletteList = mutableListOf<GetNewRouletteResponse>()
fun plusWeight(optionIndex: Int) {
val currentOption = options[optionIndex]
@ -113,66 +125,166 @@ class RouletteSettingsViewModel(private val repository: RouletteRepository) : Ba
if (!_isLoading.value!!) {
_isLoading.value = true
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))
if (rouletteId > 0) {
updateRoulette(onSuccess)
} else {
createRoulette(onSuccess)
}
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!!) {
_isLoading.value = true
compositeDisposable.add(
repository.getRoulette(
repository.getAllRoulette(
creatorId = SharedPreferenceManager.userId,
token = "Bearer ${SharedPreferenceManager.token}"
)
@ -182,30 +294,9 @@ class RouletteSettingsViewModel(private val repository: RouletteRepository) : Ba
{
if (it.success) {
val data = it.data
if (data != null && data.items.isNotEmpty()) {
_isActiveLiveData.value = data.isActive
_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)
}
rouletteList.clear()
rouletteList.addAll(data ?: listOf())
selectRoulette(SelectedRoulette.ROULETTE_1)
} else {
if (it.message != null) {
_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>) {
this.options.clear()
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)
checkPermissions()
pushTokenUpdate()
gaidUpdate()
getMemberInfo()
getEventPopup()
@ -338,6 +339,10 @@ class MainActivity : BaseActivity<ActivityMainBinding>(ActivityMainBinding::infl
}
}
private fun gaidUpdate() {
viewModel.fetchAndUpdateGaid(context = applicationContext)
}
private fun getMemberInfo() {
viewModel.getMemberInfo {
notificationSettingsDialog.show(screenWidth)

View File

@ -1,7 +1,9 @@
package kr.co.vividnext.sodalive.main
import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.google.android.gms.ads.identifier.AdvertisingIdClient
import com.google.gson.annotations.SerializedName
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
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.notification.UpdateNotificationSettingRequest
import kr.co.vividnext.sodalive.user.UserRepository
import java.util.concurrent.Executors
class MainViewModel(
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() {
handler.post {
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 kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.mypage.can.charge.CanResponse
import kr.co.vividnext.sodalive.mypage.can.charge.ChargeRequest
import kr.co.vividnext.sodalive.mypage.can.charge.ChargeResponse
import kr.co.vividnext.sodalive.mypage.can.charge.VerifyRequest
import kr.co.vividnext.sodalive.mypage.can.charge.iap.GoogleChargeRequest
import kr.co.vividnext.sodalive.mypage.can.charge.pg.CanResponse
import kr.co.vividnext.sodalive.mypage.can.charge.pg.ChargeRequest
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.status.GetCanStatusResponse
import kr.co.vividnext.sodalive.mypage.can.status.charge.GetCanChargeStatusResponseItem
@ -17,6 +18,12 @@ import retrofit2.http.POST
import retrofit2.http.Query
interface CanApi {
@POST("/charge/google")
fun googleChargeCan(
@Body request: GoogleChargeRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@POST("/charge")
fun chargeCan(
@Body chargeRequest: ChargeRequest,

View File

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

View File

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

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.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.moneyFormat
class CanChargeAdapter(
class CanChargePgAdapter(
private val onClick: (CanResponse) -> Unit
) : RecyclerView.Adapter<CanChargeAdapter.ViewHolder>() {
) : RecyclerView.Adapter<CanChargePgAdapter.ViewHolder>() {
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.MutableLiveData
@ -9,7 +9,7 @@ import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
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>>()
val canChargeLiveData: LiveData<List<CanResponse>>
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 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 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.moneyFormat
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.VerifyRequest
import kr.co.vividnext.sodalive.mypage.can.charge.pg.CanResponse
import kr.co.vividnext.sodalive.mypage.can.charge.pg.VerifyRequest
import kr.co.vividnext.sodalive.mypage.can.status.CanStatusActivity
import org.koin.android.ext.android.inject
@ -140,8 +140,8 @@ class CanPaymentActivity : BaseActivity<ActivityCanPaymentBinding>(
R.font.gmarket_sans_bold
)
view.setTextColor(ContextCompat.getColor(applicationContext, R.color.color_9970ff))
view.setBackgroundResource(R.drawable.bg_round_corner_10_4d9970ff_9970ff)
view.setTextColor(ContextCompat.getColor(applicationContext, R.color.color_3bb9f1))
view.setBackgroundResource(R.drawable.bg_round_corner_10_13181b_3bb9f1)
}
private fun requestCharge() {
@ -211,15 +211,13 @@ class CanPaymentActivity : BaseActivity<ActivityCanPaymentBinding>(
request,
onSuccess = {
Toast.makeText(applicationContext, "캔이 충전되었습니다", Toast.LENGTH_LONG).show()
SharedPreferenceManager.can += (canResponse!!.rewardCan + canResponse!!.can)
if (gotoPrevPage) {
setResult(RESULT_OK)
} else {
val intent = Intent(applicationContext, CanStatusActivity::class.java)
startActivity(intent)
}
SharedPreferenceManager.can += (canResponse!!.rewardCan + canResponse!!.can)
finish()
},
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.common.SharedPreferenceManager
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.VerifyRequest
import kr.co.vividnext.sodalive.mypage.can.charge.pg.ChargeRequest
import kr.co.vividnext.sodalive.mypage.can.charge.pg.VerifyRequest
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_3))
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
}

View File

@ -6,6 +6,7 @@ import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.os.bundleOf
import com.google.firebase.dynamiclinks.PendingDynamicLinkData
import com.google.firebase.dynamiclinks.ktx.dynamicLinks
@ -32,6 +33,9 @@ class SplashActivity : BaseActivity<ActivitySplashBinding>(ActivitySplashBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val lp = binding.ivText.layoutParams as ConstraintLayout.LayoutParams
lp.topMargin = screenHeight * 787 / 2337
binding.ivText.layoutParams = lp
setupRemoteConfig()
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.explorer.profile.MemberBlockRequest
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.mypage.MyPageResponse
import kr.co.vividnext.sodalive.mypage.profile.ProfileResponse
@ -137,4 +138,10 @@ interface UserApi {
@Part multipartFile: MultipartBody.Part,
@Header("Authorization") authHeader: 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.explorer.profile.MemberBlockRequest
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.mypage.MyPageResponse
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>> {
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"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/color_4d9970ff" />
<corners android:radius="10dp" />
<solid android:color="@color/color_111111" />
<corners android:radius="13.3dp" />
<stroke
android:width="1dp"
android:color="@color/color_9970ff" />
android:color="@color/color_111111" />
</shape>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<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" />
<stroke
android:width="1.3dp"
android:color="@color/color_9970ff" />
android:width="1dp"
android:color="@color/color_777777" />
</shape>

View File

@ -75,7 +75,7 @@
android:layout_alignParentEnd="true"
android:contentDescription="@null"
android:visibility="gone"
tools:src="@drawable/btn_notification_selected" />
tools:src="@drawable/btn_following_big" />
</RelativeLayout>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
@ -122,13 +122,13 @@
android:id="@+id/tv_preview_no"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:background="@drawable/bg_round_corner_46_7_333333"
android:padding="13.3dp"
android:visibility="gone"
android:layout_centerInParent="true"
android:text="해당 콘텐츠는 크리에이터의 요청으로\n미리듣기를 제공하지 않습니다"
android:textColor="@color/color_eeeeee"
android:textSize="16.7sp" />
android:textSize="16.7sp"
android:visibility="gone" />
<SeekBar
android:id="@+id/sb_progress"
@ -140,6 +140,24 @@
android:paddingEnd="0dp"
android:progressDrawable="@drawable/audio_content_player_seekbar"
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>
<androidx.constraintlayout.widget.ConstraintLayout
@ -364,6 +382,75 @@
</LinearLayout>
</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
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -422,6 +509,21 @@
</RelativeLayout>
</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
android:id="@+id/ll_purchase"
android:layout_width="match_parent"
@ -579,6 +681,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="11dp"
android:ellipsize="end"
android:lineSpacingExtra="8dp"
android:maxLines="1"
android:textColor="@color/color_bbbbbb"
android:textSize="12sp"

View File

@ -1,20 +1,29 @@
<?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_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
android:orientation="vertical">
<include
android:id="@+id/toolbar"
layout="@layout/detail_toolbar" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_charge_can"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="50dp"
android:visibility="gone"
app:tabIndicatorColor="@color/color_80d8ff"
app:tabIndicatorFullWidth="true"
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"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
android:orientation="vertical">
<include

View File

@ -11,12 +11,12 @@
android:id="@+id/iv_cover"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="16dp"
android:contentDescription="@null"
android:scaleType="fitCenter"
android:layout_marginTop="16dp"
app:layout_constraintBottom_toTopOf="@+id/rl_input_chat"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="@+id/rl_input_chat"
app:layout_constraintTop_toBottomOf="@id/ll_top" />
<View
@ -104,6 +104,59 @@
</ScrollView>
</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
android:id="@+id/ll_top"
android:layout_width="0dp"
@ -143,6 +196,22 @@
android:gravity="end"
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
android:id="@+id/tv_bg_switch"
android:layout_width="wrap_content"
@ -225,9 +294,9 @@
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginHorizontal="5.3dp"
android:layout_toStartOf="@+id/iv_creator_follow"
android:layout_toEndOf="@+id/rl_creator_profile"
android:orientation="vertical">
@ -262,27 +331,33 @@
tools:text="오늘 라이브 방송은ㅇㄹ너ㅏㅣㅇㄴ럴ㄴ아ㅣㄴㅇ러ㅏㅣ" />
</LinearLayout>
<TextView
android:id="@+id/tv_creator_nickname"
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2.7dp"
android:ellipsize="end"
android:fontFamily="@font/gmarket_sans_medium"
android:lines="1"
android:textColor="@color/color_777777"
android:textSize="12sp"
tools:text="청령" />
</LinearLayout>
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:id="@+id/iv_creator_follow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:contentDescription="@null"
android:src="@drawable/btn_plus_round" />
<TextView
android:id="@+id/tv_creator_nickname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2.7dp"
android:ellipsize="end"
android:fontFamily="@font/gmarket_sans_medium"
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>
<androidx.recyclerview.widget.RecyclerView
@ -308,7 +383,6 @@
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:background="@drawable/bg_round_corner_5_3_transparent_bbbbbb"
android:drawablePadding="2.7dp"
android:fontFamily="@font/gmarket_sans_medium"
android:paddingHorizontal="8dp"
android:paddingVertical="5.3dp"
@ -317,6 +391,23 @@
android:textSize="12sp"
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
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -357,7 +448,7 @@
android:id="@+id/ll_view_users"
android:layout_width="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:orientation="horizontal"
android:paddingHorizontal="11dp"
@ -571,4 +662,16 @@
android:contentDescription="@null"
android:src="@drawable/btn_message_send" />
</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>

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