From 0cbf2abf5eaeee56991934d8603bf4c1a9980fe2 Mon Sep 17 00:00:00 2001 From: klaus Date: Sun, 30 Jul 2023 21:16:16 +0900 Subject: [PATCH] =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=EB=B0=A9=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EB=B3=B4=EA=B8=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kr/co/vividnext/sodalive/live/LiveApi.kt | 10 + .../vividnext/sodalive/live/LiveFragment.kt | 35 +- .../vividnext/sodalive/live/LiveRepository.kt | 10 + .../live/room/detail/GetRoomDetailResponse.kt | 44 ++ .../live/room/detail/LiveRoomDetailAdapter.kt | 50 ++ .../room/detail/LiveRoomDetailFragment.kt | 324 +++++++++++++ .../room/detail/LiveRoomDetailViewModel.kt | 66 +++ .../res/drawable-xxhdpi/btn_big_share.png | Bin 0 -> 3017 bytes .../res/drawable-xxhdpi/ic_blog_purple.png | Bin 0 -> 1166 bytes .../res/drawable-xxhdpi/ic_close_white.png | Bin 0 -> 386 bytes .../drawable-xxhdpi/ic_instagram_purple.png | Bin 0 -> 1140 bytes .../drawable-xxhdpi/ic_live_detail_bottom.png | Bin 0 -> 281 bytes .../drawable-xxhdpi/ic_live_detail_top.png | Bin 0 -> 280 bytes .../res/drawable-xxhdpi/ic_thumb_play.png | Bin 0 -> 702 bytes .../res/drawable-xxhdpi/ic_website_purple.png | Bin 0 -> 926 bytes .../ic_youtube_play_purple.png | Bin 0 -> 858 bytes .../drawable/bg_round_corner_10_525252.xml | 8 + .../drawable/bg_round_corner_10_ff5c49.xml | 8 + .../bg_round_corner_10_transparent_9970ff.xml | 8 + .../drawable/bg_round_corner_16_7_9970ff.xml | 8 + ...bg_round_corner_5_3_transparent_dd4500.xml | 8 + .../res/layout/fragment_live_room_detail.xml | 430 ++++++++++++++++++ .../layout/item_live_detail_user_summary.xml | 8 + .../res/layout/item_live_room_detail_user.xml | 27 ++ app/src/main/res/values/colors.xml | 2 + 25 files changed, 1044 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/detail/GetRoomDetailResponse.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/detail/LiveRoomDetailAdapter.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/detail/LiveRoomDetailFragment.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/room/detail/LiveRoomDetailViewModel.kt create mode 100644 app/src/main/res/drawable-xxhdpi/btn_big_share.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_blog_purple.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_close_white.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_instagram_purple.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_live_detail_bottom.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_live_detail_top.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_thumb_play.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_website_purple.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_youtube_play_purple.png create mode 100644 app/src/main/res/drawable/bg_round_corner_10_525252.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_10_ff5c49.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_10_transparent_9970ff.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_16_7_9970ff.xml create mode 100644 app/src/main/res/drawable/bg_round_corner_5_3_transparent_dd4500.xml create mode 100644 app/src/main/res/layout/fragment_live_room_detail.xml create mode 100644 app/src/main/res/layout/item_live_detail_user_summary.xml create mode 100644 app/src/main/res/layout/item_live_room_detail_user.xml diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/LiveApi.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/LiveApi.kt index fc0d70d..80de8ae 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/LiveApi.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/LiveApi.kt @@ -1,10 +1,13 @@ package kr.co.vividnext.sodalive.live import io.reactivex.rxjava3.core.Flowable +import io.reactivex.rxjava3.core.Single import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.live.room.LiveRoomStatus +import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailResponse import retrofit2.http.GET import retrofit2.http.Header +import retrofit2.http.Path import retrofit2.http.Query interface LiveApi { @@ -17,4 +20,11 @@ interface LiveApi { @Query("size") size: Int, @Header("Authorization") authHeader: String ): Flowable>> + + @GET("/live/room/detail/{id}") + fun getRoomDetail( + @Path("id") id: Long, + @Query("timezone") timezone: String, + @Header("Authorization") authHeader: String + ): Single> } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/LiveFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/LiveFragment.kt index 7cf3dfe..771aca9 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/LiveFragment.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/LiveFragment.kt @@ -26,6 +26,7 @@ import kr.co.vividnext.sodalive.live.now.LiveNowAdapter import kr.co.vividnext.sodalive.live.recommend.RecommendLiveAdapter import kr.co.vividnext.sodalive.live.recommend_channel.LiveRecommendChannelAdapter import kr.co.vividnext.sodalive.live.reservation.LiveReservationAdapter +import kr.co.vividnext.sodalive.live.room.detail.LiveRoomDetailFragment import kr.co.vividnext.sodalive.settings.notification.MemberRole import org.koin.android.ext.android.inject import kotlin.math.roundToInt @@ -234,7 +235,22 @@ class LiveFragment : BaseFragment(FragmentLiveBinding::infl .layoutLiveNow .rvSudaNow - liveNowAdapter = LiveNowAdapter {} + liveNowAdapter = LiveNowAdapter { + val detailFragment = LiveRoomDetailFragment( + it.roomId, + onClickParticipant = {}, + onClickReservation = {}, + onClickModify = {}, + onClickStart = {}, + onClickCancel = {} + ) + if (detailFragment.isAdded) return@LiveNowAdapter + + detailFragment.show( + requireActivity().supportFragmentManager, + detailFragment.tag + ) + } recyclerView.layoutManager = LinearLayoutManager( requireContext(), @@ -300,7 +316,22 @@ class LiveFragment : BaseFragment(FragmentLiveBinding::infl .layoutLiveReservation .rvSudaReservation - liveReservationAdapter = LiveReservationAdapter(isMain = true) {} + liveReservationAdapter = LiveReservationAdapter(isMain = true) { + val detailFragment = LiveRoomDetailFragment( + it.roomId, + onClickParticipant = {}, + onClickReservation = {}, + onClickModify = {}, + onClickStart = {}, + onClickCancel = {} + ) + if (detailFragment.isAdded) return@LiveReservationAdapter + + detailFragment.show( + requireActivity().supportFragmentManager, + detailFragment.tag + ) + } recyclerView.layoutManager = LinearLayoutManager( requireContext(), diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/LiveRepository.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/LiveRepository.kt index e0ca9e0..2e79523 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/LiveRepository.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/LiveRepository.kt @@ -1,8 +1,10 @@ package kr.co.vividnext.sodalive.live import io.reactivex.rxjava3.core.Flowable +import io.reactivex.rxjava3.core.Single import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.live.room.LiveRoomStatus +import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailResponse import java.util.TimeZone class LiveRepository(private val api: LiveApi) { @@ -22,4 +24,12 @@ class LiveRepository(private val api: LiveApi) { authHeader = token ) } + + fun getRoomDetail(roomId: Long, token: String): Single> { + return api.getRoomDetail( + roomId, + timezone = TimeZone.getDefault().id, + authHeader = token + ) + } } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/detail/GetRoomDetailResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/detail/GetRoomDetailResponse.kt new file mode 100644 index 0000000..37ff507 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/detail/GetRoomDetailResponse.kt @@ -0,0 +1,44 @@ +package kr.co.vividnext.sodalive.live.room.detail + +import android.os.Parcelable +import com.google.gson.annotations.SerializedName +import kotlinx.parcelize.Parcelize + +@Parcelize +data class GetRoomDetailResponse( + @SerializedName("roomId") val roomId: Long, + @SerializedName("price") val price: Int, + @SerializedName("title") val title: String, + @SerializedName("content") val content: String, + @SerializedName("isPaid") val isPaid: Boolean, + @SerializedName("isPrivateRoom") val isPrivateRoom: Boolean, + @SerializedName("password") val password: Int?, + @SerializedName("tags") val tags: List, + @SerializedName("channelName") val channelName: String?, + @SerializedName("beginDateTime") val beginDateTime: String, + @SerializedName("isNotification") val isNotification: Boolean, + @SerializedName("numberOfParticipants") val numberOfParticipants: Int, + @SerializedName("numberOfParticipantsTotal") val numberOfParticipantsTotal: Int, + @SerializedName("manager") val manager: GetRoomDetailManager, + @SerializedName("participatingUsers") val participatingUsers: List +): Parcelable + +@Parcelize +data class GetRoomDetailManager( + @SerializedName("id") val id: Long, + @SerializedName("nickname") val nickname: String, + @SerializedName("introduce") val introduce: String, + @SerializedName("youtubeUrl") val youtubeUrl: String?, + @SerializedName("instagramUrl") val instagramUrl: String?, + @SerializedName("websiteUrl") val websiteUrl: String?, + @SerializedName("blogUrl") val blogUrl: String?, + @SerializedName("profileImageUrl") val profileImageUrl: String, + @SerializedName("isCreator") val isCreator: Boolean +) : Parcelable + +@Parcelize +data class GetRoomDetailUser( + @SerializedName("id") val id: Long, + @SerializedName("nickname") val nickname: String, + @SerializedName("profileImageUrl") val profileImageUrl: String +) : Parcelable diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/detail/LiveRoomDetailAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/detail/LiveRoomDetailAdapter.kt new file mode 100644 index 0000000..2da2766 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/detail/LiveRoomDetailAdapter.kt @@ -0,0 +1,50 @@ +package kr.co.vividnext.sodalive.live.room.detail + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import coil.load +import coil.transform.RoundedCornersTransformation +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.databinding.ItemLiveRoomDetailUserBinding +import kr.co.vividnext.sodalive.extensions.dpToPx + +class LiveRoomDetailAdapter( + private val onClick: (GetRoomDetailUser) -> Unit +) : RecyclerView.Adapter() { + + val items = mutableListOf() + + inner class ViewHolder( + private val binding: ItemLiveRoomDetailUserBinding + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(item: GetRoomDetailUser) { + binding.tvNickname.text = item.nickname + binding.ivProfile.load(item.profileImageUrl) { + crossfade(true) + placeholder(R.drawable.bg_placeholder) + transformations(RoundedCornersTransformation(23.4f.dpToPx())) + } + binding.root.setOnClickListener { onClick(item) } + } + } + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): ViewHolder { + return ViewHolder( + ItemLiveRoomDetailUserBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(items[position]) + } + + override fun getItemCount() = items.count() +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/detail/LiveRoomDetailFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/detail/LiveRoomDetailFragment.kt new file mode 100644 index 0000000..6c8c2bf --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/detail/LiveRoomDetailFragment.kt @@ -0,0 +1,324 @@ +package kr.co.vividnext.sodalive.live.room.detail + +import android.annotation.SuppressLint +import android.content.Intent +import android.graphics.Rect +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.webkit.URLUtil +import android.widget.FrameLayout +import android.widget.LinearLayout +import android.widget.Toast +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView +import coil.load +import coil.transform.CircleCropTransformation +import coil.transform.RoundedCornersTransformation +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.common.LoadingDialog +import kr.co.vividnext.sodalive.common.SharedPreferenceManager +import kr.co.vividnext.sodalive.databinding.FragmentLiveRoomDetailBinding +import kr.co.vividnext.sodalive.databinding.ItemLiveDetailUserSummaryBinding +import kr.co.vividnext.sodalive.extensions.dpToPx +import org.koin.android.ext.android.inject + +class LiveRoomDetailFragment( + private val roomId: Long, + private val onClickParticipant: () -> Unit, + private val onClickReservation: () -> Unit, + private val onClickModify: (GetRoomDetailResponse) -> Unit, + private val onClickStart: () -> Unit, + private val onClickCancel: () -> Unit +) : BottomSheetDialogFragment() { + + private val viewModel: LiveRoomDetailViewModel by inject() + + private lateinit var binding: FragmentLiveRoomDetailBinding + + private var isAllProfileOpen = false + + private lateinit var adapter: LiveRoomDetailAdapter + private lateinit var loadingDialog: LoadingDialog + private lateinit var roomDetail: GetRoomDetailResponse + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentLiveRoomDetailBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + loadingDialog = LoadingDialog(requireActivity(), layoutInflater) + + val bottomSheet = dialog?.findViewById( + com.google.android.material.R.id.design_bottom_sheet + ) + val behavior = BottomSheetBehavior.from(bottomSheet!!) + behavior.state = BottomSheetBehavior.STATE_EXPANDED + + setupAdapter() + bindData() + viewModel.getDetail(roomId) { dismiss() } + + binding.ivClose.setOnClickListener { dismiss() } + binding.tvOpenAllProfile.setOnClickListener { + isAllProfileOpen = !isAllProfileOpen + if (isAllProfileOpen) { + binding.llProfiles.visibility = View.GONE + binding.rvParticipate.visibility = View.VISIBLE + binding.tvParticipateExpression.visibility = View.VISIBLE + binding.tvOpenAllProfile.text = "닫기" + binding.tvOpenAllProfile.setCompoundDrawablesWithIntrinsicBounds( + R.drawable.ic_live_detail_top, + 0, + 0, + 0 + ) + } else { + binding.llProfiles.visibility = View.VISIBLE + binding.rvParticipate.visibility = View.GONE + binding.tvParticipateExpression.visibility = View.GONE + binding.tvOpenAllProfile.text = "펼쳐보기" + binding.tvOpenAllProfile.setCompoundDrawablesWithIntrinsicBounds( + R.drawable.ic_live_detail_bottom, + 0, + 0, + 0 + ) + } + } + } + + private fun setupAdapter() { + val recyclerView = binding.rvParticipate + adapter = LiveRoomDetailAdapter {} + + recyclerView.layoutManager = GridLayoutManager(requireContext(), 5) + recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() { + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + super.getItemOffsets(outRect, view, parent, state) + + outRect.top = 13.3f.dpToPx().toInt() + outRect.bottom = 13.3f.dpToPx().toInt() + } + }) + recyclerView.adapter = adapter + } + + private fun bindData() { + viewModel.isLoading.observe(viewLifecycleOwner) { + if (it) { + loadingDialog.show(resources.displayMetrics.widthPixels) + } else { + loadingDialog.dismiss() + } + } + + viewModel.toastLiveData.observe(viewLifecycleOwner) { + it?.let { Toast.makeText(requireActivity(), it, Toast.LENGTH_LONG).show() } + } + + viewModel.liveRoomDetailLiveData.observe(viewLifecycleOwner) { + roomDetail = it + setRoomDetail(it) + } + } + + @SuppressLint("SetTextI18n", "NotifyDataSetChanged") + private fun setRoomDetail(response: GetRoomDetailResponse) { + binding.tvTitle.text = response.title + binding.tvDate.text = response.beginDateTime + binding.tvParticipate.text = response.numberOfParticipants.toString() + binding.tvTotal.text = "/${response.numberOfParticipantsTotal}" + + binding.tvOpenAllProfile.visibility = if (response.numberOfParticipants <= 0) { + View.GONE + } else { + View.VISIBLE + } + + if (response.price > 0) { + binding.tvCoin.text = response.price.toString() + binding.tvCoin.setCompoundDrawablesWithIntrinsicBounds( + 0, + 0, + R.drawable.ic_can, + 0 + ) + } else { + binding.tvCoin.text = "무료" + binding.tvCoin.setCompoundDrawablesWithIntrinsicBounds( + 0, + 0, + 0, + 0 + ) + } + + setManagerProfile(manager = response.manager) + setParticipantUserSummary(response.participatingUsers) + + binding.tvTags.text = response.tags.joinToString(" ") { "#$it" } + binding.tvContent.text = response.content + + if (response.channelName.isNullOrBlank()) { + binding.tvParticipateExpression.text = "예약자" + when { + response.manager.id == SharedPreferenceManager.userId -> { + binding.llStartDelete.visibility = View.VISIBLE + binding.tvReservationComplete.visibility = View.GONE + binding.tvParticipateNow.visibility = View.GONE + binding.tvReservation.visibility = View.GONE + binding.tvModify.setOnClickListener { + onClickModify(roomDetail) + dismiss() + } + binding.tvLiveStart.setOnClickListener { + onClickStart() + dismiss() + } + binding.tvLiveCancel.setOnClickListener { + onClickCancel() + dismiss() + } + } + + response.isPaid -> { + binding.tvReservationComplete.visibility = View.VISIBLE + binding.tvParticipateNow.visibility = View.GONE + binding.tvReservation.visibility = View.GONE + binding.llStartDelete.visibility = View.GONE + } + + else -> { + binding.tvReservationComplete.visibility = View.GONE + binding.tvParticipateNow.visibility = View.GONE + binding.tvReservation.visibility = View.VISIBLE + binding.tvReservation.setOnClickListener { + onClickReservation() + dismiss() + } + binding.llStartDelete.visibility = View.GONE + } + } + } else { + binding.tvParticipateExpression.text = "참여자" + binding.tvReservationComplete.visibility = View.GONE + binding.tvParticipateNow.visibility = View.VISIBLE + binding.tvReservation.visibility = View.GONE + binding.tvParticipateNow.setOnClickListener { + onClickParticipant() + dismiss() + } + binding.llStartDelete.visibility = View.GONE + } + + adapter.items.addAll(response.participatingUsers) + adapter.notifyDataSetChanged() + } + + private fun setManagerProfile(manager: GetRoomDetailManager) { + binding.tvManagerNickname.text = manager.nickname + binding.tvManagerIntroduce.text = manager.introduce + binding.ivManagerProfile.load(manager.profileImageUrl) { + crossfade(true) + placeholder(R.drawable.bg_placeholder) + transformations(CircleCropTransformation()) + } + + if ( + manager.websiteUrl.isNullOrBlank() || + !URLUtil.isValidUrl(manager.websiteUrl) + ) { + binding.ivManagerWebsite.visibility = View.GONE + } else { + binding.ivManagerWebsite.visibility = View.VISIBLE + binding.ivManagerWebsite.setOnClickListener { + startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(manager.websiteUrl))) + } + } + + if ( + manager.blogUrl.isNullOrBlank() || + !URLUtil.isValidUrl(manager.blogUrl) + ) { + binding.ivManagerBlog.visibility = View.GONE + } else { + binding.ivManagerBlog.visibility = View.VISIBLE + binding.ivManagerBlog.setOnClickListener { + startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(manager.blogUrl))) + } + } + + if ( + manager.instagramUrl.isNullOrBlank() || + !URLUtil.isValidUrl(manager.instagramUrl) + ) { + binding.ivManagerInstagram.visibility = View.GONE + } else { + binding.ivManagerInstagram.visibility = View.VISIBLE + binding.ivManagerInstagram.setOnClickListener { + startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(manager.instagramUrl))) + } + } + + if ( + manager.youtubeUrl.isNullOrBlank() || + !URLUtil.isValidUrl(manager.youtubeUrl) + ) { + binding.ivManagerYoutube.visibility = View.GONE + } else { + binding.ivManagerYoutube.visibility = View.VISIBLE + binding.ivManagerYoutube.setOnClickListener { + startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(manager.youtubeUrl))) + } + } + + if (manager.isCreator) { + binding.tvManagerProfile.visibility = View.VISIBLE + binding.tvManagerProfile.setOnClickListener {} + } else { + binding.tvManagerProfile.visibility = View.GONE + } + } + + private fun setParticipantUserSummary(participatingUsers: List) { + val userCount = if (participatingUsers.size > 10) { + 10 + } else { + participatingUsers.size + } + + for (index in 0 until userCount) { + val user = participatingUsers[index] + val itemView = ItemLiveDetailUserSummaryBinding.inflate(layoutInflater) + itemView.ivProfile.load(user.profileImageUrl) { + crossfade(true) + placeholder(R.drawable.bg_placeholder) + transformations(RoundedCornersTransformation(16.7f.dpToPx())) + } + + val lp = LinearLayout.LayoutParams(33.3f.dpToPx().toInt(), 33.3f.dpToPx().toInt()) + if (index > 0) { + lp.setMargins(-16.7f.dpToPx().toInt(), 0, 0, 0) + } + itemView.root.layoutParams = lp + + binding.llProfiles.addView(itemView.root) + } + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/detail/LiveRoomDetailViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/detail/LiveRoomDetailViewModel.kt new file mode 100644 index 0000000..d82bcd2 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/detail/LiveRoomDetailViewModel.kt @@ -0,0 +1,66 @@ +package kr.co.vividnext.sodalive.live.room.detail + +import android.net.Uri +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.google.firebase.dynamiclinks.ShortDynamicLink +import com.google.firebase.dynamiclinks.ktx.androidParameters +import com.google.firebase.dynamiclinks.ktx.dynamicLinks +import com.google.firebase.dynamiclinks.ktx.iosParameters +import com.google.firebase.dynamiclinks.ktx.shortLinkAsync +import com.google.firebase.ktx.Firebase +import com.orhanobut.logger.Logger +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.schedulers.Schedulers +import kr.co.vividnext.sodalive.base.BaseViewModel +import kr.co.vividnext.sodalive.common.SharedPreferenceManager +import kr.co.vividnext.sodalive.live.LiveRepository + +class LiveRoomDetailViewModel(private val repository: LiveRepository) : BaseViewModel() { + private val _liveRoomDetailLiveData = MutableLiveData() + val liveRoomDetailLiveData: MutableLiveData + get() = _liveRoomDetailLiveData + + private val _toastLiveData = MutableLiveData() + val toastLiveData: LiveData + get() = _toastLiveData + + private var _isLoading = MutableLiveData(false) + val isLoading: LiveData + get() = _isLoading + + fun getDetail(roomId: Long, onFailure: () -> Unit) { + if (!_isLoading.value!!) { + _isLoading.value = true + } + + compositeDisposable.add( + repository.getRoomDetail(roomId, token = "Bearer ${SharedPreferenceManager.token}") + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + _isLoading.value = false + if (it.success && it.data != null) { + _liveRoomDetailLiveData.postValue(it.data!!) + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + onFailure() + } + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + onFailure() + } + ) + ) + } +} diff --git a/app/src/main/res/drawable-xxhdpi/btn_big_share.png b/app/src/main/res/drawable-xxhdpi/btn_big_share.png new file mode 100644 index 0000000000000000000000000000000000000000..7eb49e536692fa126d28b099dfddf7431d14808e GIT binary patch literal 3017 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 gAL`q*cgqpQKfX$+VGZ zx^prwfgHsEpAc8~0-yhQK|jm6-$2D&B|(0{3~z-l#F^n# z2N~035|1galX-kfHfBdVcX{D6yK^;PKb|{z+&cf^+l`uLw~PhP)Xe?1{NQ%u?{??Y z{q)W=7gtKoIDUGIY~1qR%2?4SIiVe!ZJG7He5j=|Yjuleo{-wk&gBmj=FMr& zG3MJ`Tlga>rl#qp-*a_y9*fUQ_dfWd^LTB2P`#pGeseR&$6r8CGkCiCxvXPx#Gf+%aMMrQ=-_HyZm!5~_; zAGzh3000BSNkl_ATxg+XFeAo*BQF0jLPu;ODH!b3R90OqVvJVAjPdiDfYXK zhdid6u~?9teP@bGu+4a63r4fAO&hQo|JwlsMq&ZLHBcNQGW=T^ATu(V5SjA|ES4%id=uE->?!0%XKBFIb@kXmHS#H#N!uEj0-V`@j`%^I&439+%#- z>y?|r#Ymb{f$<2{n2QQf&cE3*uCY!ciiE$h(T!&oR4>eQToPt|Kd^OlWoiPjJ{Xl3 zF7{<56Km#Aoe&CBF}h*$8Y;E5&HjTeNnobx%U%u5XSSq(S?GFdvLywKgOAC#uw|l! zDXEB`^WiX42w<)mu7u5#!W8Q9>VmBHY)Jrf{Rsv829p3LUh|dT*98F#!oHFi24P=I z95e%0l0hHpj{v65PJOk-u=|w&Cc)JT91lKHut0n-l1FD8EzATTi*^-H`#DRp5X@&m zy)-46dcD#0tGms`?9URLX7yZ~9kDE*Ixoxgth!*{|ss3H#fi9)z#^bv- z>R17dHtNf!@Zf!;Dg1ql%a;9(F%A2u?T0~|DTB6#25oeW+O`|C2{~v>b<_s&pzZEa zoA-l`B7SwS@-h4|$3&wJy+$434LXnL literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_live_detail_bottom.png b/app/src/main/res/drawable-xxhdpi/ic_live_detail_bottom.png new file mode 100644 index 0000000000000000000000000000000000000000..ce5efb91e900554f64b0d75436000c74af263183 GIT binary patch literal 281 zcmeAS@N?(olHy`uVBq!ia0vp^HXzKx3?xrnI^qbVSkfJRf%L|H?mvmFKt5w}kh>GZ zx^prwfgI5QpAc8~0-y5o@`{RzzP>&n31kDoj_I;iKm}|iL4Lsu9zQvD$5reS=K+dL z@N{tuu{izqiZ54#f&lA-HGBe&>;;c+9S|1TqPNSj<-5Jp%)dRqmI?nbvvb>b*T8k+ zWM}0ti+5YGZ zx^prwfgI5QpAc8~0-tg)=;`UH0E0=BCPmfMIRF)~mjw9*GyL)9+&!QD$V&EnpirNu zi(`nz>9<$T@-`T7um$8iW{}vM{fNPKM?)c#`G+U}lz)V8U3%c;zopr09`9<8UO$Q literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_thumb_play.png b/app/src/main/res/drawable-xxhdpi/ic_thumb_play.png new file mode 100644 index 0000000000000000000000000000000000000000..99fc670c8cf9f7b9fb4c8d7427f98203ef9a2d24 GIT binary patch literal 702 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NU3?z3ec*FxKmUKs7M+SzC{oH>NS%G}U;vjb? zhIQv;UIIBA1AIbU-3xsFBY_{^K%8%2^6y^|``XHBF_kh^fkN>}a38a4Gr2i}0{=a+i|MPp0 z${%0qzkmLJU!uPc=y>&#AirP+gJn{` z`88PI*m<^Zt^Rg7u~>2Sp-JKMa;?tIdh&6}`;2?bZ{B#j;mU3iV3;sgc)B=-Se!mP z<#t(mW9J@^v_3h~dVyuO7k{n0 zTG41~8`G9PyKPSJ?HR`mBhR=Co%>+G@cpHklzwZ1c)|VSf1jJ(k_~H$?$us75|PpY0b*p}$O zA%MH{GxNF$vTc?NZ_d?STKk6aCpx9=_#ATctKrr?TK~QS1=dsO<&eJ zC(DdI@z4&jM>TIQ$Ti6OZ*4d?rEei)PS$yWFS~w(t+?HEXUf&bo7b$n$yf6=Yyp=@ zSl)pmzrrPVmTAf!^0HWA#l>s5Z1^@+?TF@sm?t2;i^{XgsiB2AIr7>v2WR2aN?TBmRw-8F?hQAxvXPx#El^BUMMrQ_!PlMjF|6P>;ESY0008@ zNklG5-P-NDTtsRO1$Vzh`8$}t?xHDo9<@cPG&o^uLlo)FzoD``DSO{%)a;L z0Z6!Ce{|Kx2;Rp_o#(JO9=ykKT=~8i<*SXKI!{j%$=&1i$LJkSWfb3{e@-Dk=HQoR z3gtCkR5umR<4twTA|6bg!Vz3niJ$F32}qa6g(a0RG2|;*G~q03wqen)N|KlEyoQsv zJ|_jU_94*Ww%1L(HwK_b`xkvHV9kJ&?DgXhn1FVhwrfV5g10uRpBaD|+8;($XU>q5 z^P^hXZ5nclR&*Fz*T57D{jg%yj5!52`7}Z}{=I$%PV4%v0_QSta>H#Y+VdGWc`wj$ zsYgZi1LNq>K`Vi_XMPfjgDUuhbYe4D)lw|@hHBi3yG#ZetHisLxABHzZma0UA1d?$ z6~qT;3e|_TjVtl-5lWc#$y~>6OK6kGnw>6`$q(D`ApfpADlu%@X&olX5F0)$i0MUz zjJ-`Jvjh(}iSBz)L2O1P(J;cyZ)mL~n#2ZGqoU?a^@HirrUtsBqUHz%Y}67>IlM3` zYEEGroV8@r9f*sPGwp*#&QircrG`T|c^53YR7?ZmOt`Ss%*ZM3fDIWr-~m#Oe^d>+ z9>KsA^+1~5{s+#Bp$&K=YA1JkwKhEvFUs?8Dh;L#sMYduKSetwDzG zgw4Ujcs+z;thaq~7<1$3z+Bh+`_8}DwVioxUeZ>XoHt>stZK${Eo@@K_L}@Z!uHBG zUi+U48!!11y*&`VMDkgBj__HMZ`5;yZhxdXE9nw&c==kCn9qjPcrtegWUx53Jx z2!D_{SHsE)v2stW92X~-Ch#P4&W@KGG|NH$1?YqA;Oh`#I{*Lx07*qoM6N<$f_e+I A?f?J) literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_youtube_play_purple.png b/app/src/main/res/drawable-xxhdpi/ic_youtube_play_purple.png new file mode 100644 index 0000000000000000000000000000000000000000..9ac3995d2d7ff20f422505bca02ab194b0fcbc23 GIT binary patch literal 858 zcmV-g1Eu_lP)Px#El^BUMMrQ`Uw5)?hV;%r@r+=z6<$!>C?Q|9dPEKEPR#GLoH#wlkwY(@%2Ey zoYf$U@?`cU>#97LePvrd&_2RrIg>T~z7LW>XL|=%vIfyDZ}vQLFyr-Qob5Qhq|f2n zfUV2Bukb)o+=oGSE80irVbM@%aQ?uXZ`pMCZbd1+Eg9(ZAbb zi8TU~kCHE<@CztbOBOVCNMbfN6UUn%F(Zke)=DJCZY{+Tvn0lfJSfIMvtS=`$3$kQ z0wae+W@7@bY!aEVpHTloWKJ}pTtOqt_tFt7ntX$1}VKOsI4Oqsu%8qZXYSDdr|1sjy*Z0R z@q?#B@$1N^Gig=C4AKx^!-QBvkXF?+OpLV(X<}K!9AiyH+Iw5WOkwRs8gFZuEv)f4 zmKel^RxH6WtB|FNN2OHEO2x)Ydp*?YYw6g?iNTy$&&eq`xeF&p;^bnioR5=Ra&lNs zuFT2FS-C%_#^BU4teS~an{jGDPOZtRX*snst48Pa0$4o + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_10_ff5c49.xml b/app/src/main/res/drawable/bg_round_corner_10_ff5c49.xml new file mode 100644 index 0000000..bc314a6 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_10_ff5c49.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_10_transparent_9970ff.xml b/app/src/main/res/drawable/bg_round_corner_10_transparent_9970ff.xml new file mode 100644 index 0000000..51b77c7 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_10_transparent_9970ff.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_16_7_9970ff.xml b/app/src/main/res/drawable/bg_round_corner_16_7_9970ff.xml new file mode 100644 index 0000000..cc31a19 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_16_7_9970ff.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_5_3_transparent_dd4500.xml b/app/src/main/res/drawable/bg_round_corner_5_3_transparent_dd4500.xml new file mode 100644 index 0000000..30b765d --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_5_3_transparent_dd4500.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/layout/fragment_live_room_detail.xml b/app/src/main/res/layout/fragment_live_room_detail.xml new file mode 100644 index 0000000..8f9c1e0 --- /dev/null +++ b/app/src/main/res/layout/fragment_live_room_detail.xml @@ -0,0 +1,430 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_live_detail_user_summary.xml b/app/src/main/res/layout/item_live_detail_user_summary.xml new file mode 100644 index 0000000..dda6230 --- /dev/null +++ b/app/src/main/res/layout/item_live_detail_user_summary.xml @@ -0,0 +1,8 @@ + + diff --git a/app/src/main/res/layout/item_live_room_detail_user.xml b/app/src/main/res/layout/item_live_room_detail_user.xml new file mode 100644 index 0000000..a6c9d7c --- /dev/null +++ b/app/src/main/res/layout/item_live_room_detail_user.xml @@ -0,0 +1,27 @@ + + + + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index cdd70ad..3f10932 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -33,4 +33,6 @@ #339970FF #7FE2E2E2 #4D9970FF + #525252 + #DD4500