diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/AudioContentApi.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/AudioContentApi.kt index 7bfa0b9..8046c3b 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/AudioContentApi.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/AudioContentApi.kt @@ -15,10 +15,10 @@ import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentBannerResponse import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentCurationResponse import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentRanking -import kr.co.vividnext.sodalive.audio_content.main.GetNewContentUploadCreator +import kr.co.vividnext.sodalive.audio_content.main.v2.GetPopularContentByCreatorResponse +import kr.co.vividnext.sodalive.audio_content.main.v2.home.GetContentMainTabHomeResponse import kr.co.vividnext.sodalive.audio_content.order.GetAudioContentOrderListResponse import kr.co.vividnext.sodalive.audio_content.order.OrderRequest -import kr.co.vividnext.sodalive.audio_content.order.OrderType import kr.co.vividnext.sodalive.audio_content.player.GenerateUrlResponse import kr.co.vividnext.sodalive.audio_content.upload.theme.GetAudioContentThemeResponse import kr.co.vividnext.sodalive.common.ApiResponse @@ -211,11 +211,6 @@ interface AudioContentApi { @Header("Authorization") authHeader: String ): Single>> - @GET("/audio-content/main/new-content-upload-creator") - fun getNewContentUploadCreatorList( - @Header("Authorization") authHeader: String - ): Single>> - @GET("/audio-content/main/banner-list") fun getMainBannerList( @Header("Authorization") authHeader: String @@ -243,4 +238,15 @@ interface AudioContentApi { @Path("id") contentId: Long, @Header("Authorization") authHeader: String ): Single> + + @GET("/v2/audio-content/main/home") + fun getContentMainHome( + @Header("Authorization") authHeader: String + ): Single> + + @GET("/v2/audio-content/main/home/popular-content-by-creator") + fun getPopularContentByCreator( + @Query("creatorId") creatorId: Long, + @Header("Authorization") authHeader: String + ): Single> } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/AudioContentRepository.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/AudioContentRepository.kt index 5c8f0e6..e737127 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/AudioContentRepository.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/AudioContentRepository.kt @@ -198,10 +198,6 @@ class AudioContentRepository( authHeader = token ) - fun getNewContentUploadCreatorList( - token: String - ) = api.getNewContentUploadCreatorList(authHeader = token) - fun getMainBannerList(token: String) = api.getMainBannerList(authHeader = token) fun getMainOrderList(token: String) = api.getMainOrderList(authHeader = token) fun pinContent( diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/GetAudioContentMainResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/GetAudioContentMainResponse.kt index ba90553..e933bbd 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/GetAudioContentMainResponse.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/GetAudioContentMainResponse.kt @@ -5,7 +5,7 @@ import com.google.gson.annotations.SerializedName import kr.co.vividnext.sodalive.settings.event.EventItem @Keep -data class GetNewContentUploadCreator( +data class ContentCreatorResponse( @SerializedName("creatorId") val creatorId: Long, @SerializedName("creatorNickname") val creatorNickname: String, @SerializedName("creatorProfileImageUrl") val creatorProfileImageUrl: String @@ -39,7 +39,8 @@ data class GetAudioContentRankingItem( @SerializedName("price") val price: Int, @SerializedName("duration") val duration: String, @SerializedName("creatorId") val creatorId: Long, - @SerializedName("creatorNickname") val creatorNickname: String + @SerializedName("creatorNickname") val creatorNickname: String, + @SerializedName("creatorProfileImageUrl") val creatorProfileImageUrl: String ) @Keep diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/v2/ContentRankCreatorAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/v2/ContentRankCreatorAdapter.kt new file mode 100644 index 0000000..ab2835f --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/v2/ContentRankCreatorAdapter.kt @@ -0,0 +1,87 @@ +package kr.co.vividnext.sodalive.audio_content.main.v2 + +import android.annotation.SuppressLint +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView +import coil.load +import coil.transform.CircleCropTransformation +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.audio_content.main.ContentCreatorResponse +import kr.co.vividnext.sodalive.databinding.ItemContentRankCreatorBinding + +class ContentRankCreatorAdapter( + private val onClickItem: (Long) -> Unit, +) : RecyclerView.Adapter() { + + private var selectedCreatorId: Long = 0 + private val items = mutableListOf() + + inner class ViewHolder( + private val context: Context, + private val binding: ItemContentRankCreatorBinding + ) : RecyclerView.ViewHolder(binding.root) { + @SuppressLint("NotifyDataSetChanged") + fun bind(item: ContentCreatorResponse) { + binding.root.setOnClickListener { + selectedCreatorId = item.creatorId + onClickItem(item.creatorId) + notifyDataSetChanged() + } + + binding.tvNickname.text = item.creatorNickname + binding.ivProfile.load(item.creatorProfileImageUrl) { + transformations(CircleCropTransformation()) + placeholder(R.drawable.ic_place_holder) + crossfade(true) + } + + if (item.creatorId == selectedCreatorId) { + binding.ivBg.setImageResource(R.drawable.bg_circle_3bb9f1) + binding.ivBg.visibility = View.VISIBLE + binding.tvNickname.setTextColor( + ContextCompat.getColor( + context, + R.color.color_3bb9f1 + ) + ) + } else { + binding.ivBg.setImageResource(0) + binding.ivBg.visibility = View.GONE + binding.tvNickname.setTextColor( + ContextCompat.getColor( + context, + R.color.color_bbbbbb + ) + ) + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( + parent.context, + ItemContentRankCreatorBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(items[position]) + } + + override fun getItemCount() = items.size + + @SuppressLint("NotifyDataSetChanged") + fun addItems(items: List) { + this.items.addAll(items) + if (this.items.isNotEmpty()) { + this.selectedCreatorId = this.items[0].creatorId + } + notifyDataSetChanged() + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/v2/GetPopularContentByCreatorResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/v2/GetPopularContentByCreatorResponse.kt new file mode 100644 index 0000000..c4b376a --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/v2/GetPopularContentByCreatorResponse.kt @@ -0,0 +1,13 @@ +package kr.co.vividnext.sodalive.audio_content.main.v2 + +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName +import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentRankingItem + +@Keep +data class GetPopularContentByCreatorResponse( + @SerializedName("salesRankContentList") + val salesRankContentList: List, + @SerializedName("salesCountRankContentList") + val salesCountRankContentList: List +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/v2/home/AudioContentMainTabHomeFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/v2/home/AudioContentMainTabHomeFragment.kt new file mode 100644 index 0000000..3b92650 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/v2/home/AudioContentMainTabHomeFragment.kt @@ -0,0 +1,690 @@ +package kr.co.vividnext.sodalive.audio_content.main.v2.home + +import android.content.Intent +import android.graphics.Color +import android.graphics.Rect +import android.net.Uri +import android.os.Bundle +import android.text.SpannableString +import android.text.Spanned +import android.text.style.ForegroundColorSpan +import android.view.View +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import android.widget.Toast +import androidx.core.content.ContextCompat +import androidx.media3.common.util.UnstableApi +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import coil.load +import coil.transform.CircleCropTransformation +import coil.transform.RoundedCornersTransformation +import com.orhanobut.logger.Logger +import com.zhpan.bannerview.BaseBannerAdapter +import com.zhpan.indicator.enums.IndicatorSlideMode +import com.zhpan.indicator.enums.IndicatorStyle +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.audio_content.all.AudioContentRankingAllActivity +import kr.co.vividnext.sodalive.audio_content.box.AudioContentBoxActivity +import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity +import kr.co.vividnext.sodalive.audio_content.main.AudioContentBannerType +import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentRankingItem +import kr.co.vividnext.sodalive.audio_content.main.banner.AudioContentMainBannerAdapter +import kr.co.vividnext.sodalive.audio_content.main.new_content.AudioContentMainNewContentThemeAdapter +import kr.co.vividnext.sodalive.audio_content.main.ranking.AudioContentMainRankingAdapter +import kr.co.vividnext.sodalive.audio_content.main.v2.ContentRankCreatorAdapter +import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity +import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadActivity +import kr.co.vividnext.sodalive.base.BaseFragment +import kr.co.vividnext.sodalive.common.Constants +import kr.co.vividnext.sodalive.common.LoadingDialog +import kr.co.vividnext.sodalive.common.SharedPreferenceManager +import kr.co.vividnext.sodalive.databinding.FragmentAudioContentMainTabHomeBinding +import kr.co.vividnext.sodalive.explorer.ExplorerSectionAdapter +import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity +import kr.co.vividnext.sodalive.explorer.profile.series.UserProfileSeriesListAdapter +import kr.co.vividnext.sodalive.extensions.dpToPx +import kr.co.vividnext.sodalive.live.event_banner.EventBannerAdapter +import kr.co.vividnext.sodalive.main.MainActivity +import kr.co.vividnext.sodalive.mypage.alarm.AlarmListActivity +import kr.co.vividnext.sodalive.settings.event.EventDetailActivity +import kr.co.vividnext.sodalive.settings.notice.NoticeDetailActivity +import kr.co.vividnext.sodalive.settings.notification.MemberRole +import org.koin.android.ext.android.inject +import kotlin.math.roundToInt + +@UnstableApi +class AudioContentMainTabHomeFragment : BaseFragment( + FragmentAudioContentMainTabHomeBinding::inflate +) { + private val viewModel: AudioContentMainTabHomeViewModel by inject() + + private lateinit var loadingDialog: LoadingDialog + private lateinit var rankCreatorAdapter: ExplorerSectionAdapter + private lateinit var rankSeriesAdapter: UserProfileSeriesListAdapter + private lateinit var contentBannerAdapter: AudioContentMainBannerAdapter + private lateinit var rankContentAdapter: AudioContentMainRankingAdapter + private lateinit var rankContentSortAdapter: AudioContentMainNewContentThemeAdapter + private lateinit var contentRankCreatorAdapter: ContentRankCreatorAdapter + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupView() + bindData() + + viewModel.fetchData() + } + + private fun bindData() { + viewModel.toastLiveData.observe(viewLifecycleOwner) { + it?.let { Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() } + } + + viewModel.isLoading.observe(viewLifecycleOwner) { + if (it) { + loadingDialog.show(screenWidth) + } else { + loadingDialog.dismiss() + } + } + + viewModel.salesRankContentListLiveData.observe(viewLifecycleOwner) { + if (it.isNotEmpty()) { + binding.llNoItems.visibility = View.GONE + binding.llSalesTop2.visibility = View.VISIBLE + + setSalesRankContent( + item = it[0], + titleTextView = binding.tvSalesTitle1, + coverImageView = binding.ivSales1, + creatorTextView = binding.tvSalesCreator1, + creatorImageView = binding.ivSalesCreator1 + ) + + if (it.size > 1) { + binding.llSales2.visibility = View.VISIBLE + setSalesRankContent( + item = it[1], + titleTextView = binding.tvSalesTitle2, + coverImageView = binding.ivSales2, + creatorTextView = binding.tvSalesCreator2, + creatorImageView = binding.ivSalesCreator2 + ) + } + } + } + + viewModel.salesCountRankContentListLiveData.observe(viewLifecycleOwner) { + if (it.isNotEmpty()) { + binding.llNoItems.visibility = View.GONE + binding.llSalesCountTop2.visibility = View.VISIBLE + + setSalesRankContent( + item = it[0], + titleTextView = binding.tvSalesCountTitle1, + coverImageView = binding.ivSalesCount1, + creatorTextView = binding.tvSalesCountCreator1, + creatorImageView = binding.ivSalesCountCreator1 + ) + + if (it.size > 1) { + binding.llSalesCount2.visibility = View.VISIBLE + setSalesRankContent( + item = it[1], + titleTextView = binding.tvSalesCountTitle2, + coverImageView = binding.ivSalesCount2, + creatorTextView = binding.tvSalesCountCreator2, + creatorImageView = binding.ivSalesCountCreator2 + ) + } + } + } + } + + private fun setSalesRankContent( + item: GetAudioContentRankingItem, + titleTextView: TextView, + creatorTextView: TextView, + coverImageView: ImageView, + creatorImageView: ImageView + ) { + coverImageView.load(item.coverImageUrl) { + crossfade(true) + placeholder(R.drawable.bg_placeholder) + transformations(RoundedCornersTransformation(5.3f.dpToPx())) + } + titleTextView.text = item.title + creatorTextView.text = item.creatorNickname + creatorImageView.load(item.creatorProfileImageUrl) { + transformations(CircleCropTransformation()) + placeholder(R.drawable.ic_place_holder) + crossfade(true) + } + + coverImageView.setOnClickListener { + startActivity( + Intent(requireActivity(), AudioContentDetailActivity::class.java).apply { + putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, item.contentId) + } + ) + } + + creatorImageView.setOnClickListener { + startActivity( + Intent(requireActivity(), UserProfileActivity::class.java).apply { + putExtra(Constants.EXTRA_USER_ID, item.creatorId) + } + ) + } + } + + private fun setupView() { + loadingDialog = LoadingDialog(requireActivity(), layoutInflater) + + if (SharedPreferenceManager.role == MemberRole.CREATOR.name) { + binding.llUploadContent.visibility = View.VISIBLE + binding.llUploadContent.setOnClickListener { + startActivity( + Intent( + requireActivity(), + AudioContentUploadActivity::class.java + ) + ) + } + } else { + binding.llUploadContent.visibility = View.GONE + } + + binding.ivContentKeep.setOnClickListener { + startActivity( + Intent( + requireContext(), + AudioContentBoxActivity::class.java + ) + ) + } + + binding.ivAlarm.setOnClickListener { + startActivity( + Intent( + requireActivity(), + AlarmListActivity::class.java + ) + ) + } + + binding.flSearchChannel.setOnClickListener { + (requireActivity() as MainActivity).showSearchBar() + } + + setupNotice() + setupContentBanner() + setupCategory() + setupRankCreator() + setupRankSeries() + setupRankContentSortType() + setupRankContent() + setupEventBanner() + setupPopularContentByCreator() + } + + private fun setupNotice() { + viewModel.noticeLiveData.observe(viewLifecycleOwner) { notice -> + binding.tvNoticeTitle.text = notice.title + binding.tvDetail.setOnClickListener { + startActivity( + Intent(requireContext(), NoticeDetailActivity::class.java).apply { + putExtra(Constants.EXTRA_NOTICE, notice) + } + ) + } + } + } + + private fun setupContentBanner() { + val layoutParams = binding + .rvBanner + .layoutParams as LinearLayout.LayoutParams + + val pagerWidth = screenWidth.toDouble() - 26.7f.dpToPx() + val pagerHeight = (pagerWidth * 0.53).roundToInt() + layoutParams.width = pagerWidth.roundToInt() + layoutParams.height = pagerHeight + + contentBannerAdapter = AudioContentMainBannerAdapter( + requireContext(), + pagerWidth.roundToInt(), + pagerHeight + ) { + when (it.type) { + AudioContentBannerType.EVENT -> { + startActivity( + Intent(requireContext(), EventDetailActivity::class.java).apply { + putExtra(Constants.EXTRA_EVENT, it.eventItem!!) + } + ) + } + + AudioContentBannerType.CREATOR -> { + startActivity( + Intent(requireContext(), UserProfileActivity::class.java).apply { + putExtra(Constants.EXTRA_USER_ID, it.creatorId!!) + } + ) + } + + AudioContentBannerType.SERIES -> { + startActivity( + Intent(requireContext(), SeriesDetailActivity::class.java).apply { + putExtra(Constants.EXTRA_SERIES_ID, it.seriesId!!) + } + ) + } + + AudioContentBannerType.LINK -> { + startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it.link!!))) + } + } + } + + binding + .rvBanner + .layoutParams = layoutParams + + binding.rvBanner.apply { + adapter = contentBannerAdapter as BaseBannerAdapter + + setLifecycleRegistry(lifecycle) + setScrollDuration(1000) + setInterval(4 * 1000) + }.create() + + binding + .rvBanner + .setIndicatorView(binding.indicatorBanner) + .setIndicatorStyle(IndicatorStyle.ROUND_RECT) + .setIndicatorSlideMode(IndicatorSlideMode.SMOOTH) + .setIndicatorVisibility(View.GONE) + .setIndicatorSliderColor( + ContextCompat.getColor(requireContext(), R.color.color_909090), + ContextCompat.getColor(requireContext(), R.color.color_3bb9f1) + ) + .setIndicatorSliderWidth(4f.dpToPx().toInt(), 10f.dpToPx().toInt()) + .setIndicatorHeight(4f.dpToPx().toInt()) + + viewModel.contentBannerLiveData.observe(viewLifecycleOwner) { + if (contentBannerAdapter.itemCount <= 0 && it.isEmpty()) { + binding.rvBanner.visibility = View.GONE + binding.indicatorBanner.visibility = View.GONE + } else { + binding.rvBanner.visibility = View.VISIBLE + binding.indicatorBanner.visibility = View.VISIBLE + binding.rvBanner.refreshData(it) + } + } + } + + private fun setupCategory() { + binding.rlCategoryAudioBook.setOnClickListener { + showToast("준비중 입니다.") + } + + binding.rlCategoryAudioToon.setOnClickListener { + showToast("준비중 입니다.") + } + } + + private fun setupRankCreator() { + rankCreatorAdapter = ExplorerSectionAdapter( + onClickItem = { + startActivity( + Intent(requireContext(), UserProfileActivity::class.java).apply { + putExtra(Constants.EXTRA_USER_ID, it) + } + ) + }, + isVisibleRanking = true + ) + + binding.rvCreatorRank.layoutManager = LinearLayoutManager( + context, + LinearLayoutManager.HORIZONTAL, + false + ) + + binding.rvCreatorRank.addItemDecoration(object : RecyclerView.ItemDecoration() { + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + super.getItemOffsets(outRect, view, parent, state) + + when (parent.getChildAdapterPosition(view)) { + 0 -> { + outRect.left = 0 + outRect.right = 6.7f.dpToPx().toInt() + } + + rankCreatorAdapter.itemCount - 1 -> { + outRect.left = 6.7f.dpToPx().toInt() + outRect.right = 0 + } + + else -> { + outRect.left = 6.7f.dpToPx().toInt() + outRect.right = 6.7f.dpToPx().toInt() + } + } + } + }) + + binding.rvCreatorRank.adapter = rankCreatorAdapter + + viewModel.rankCreatorLiveData.observe(viewLifecycleOwner) { + binding.tvDesc.text = it.desc + binding.tvCreatorRankTitle.text = if ( + !it.coloredTitle.isNullOrBlank() && + !it.color.isNullOrBlank() + ) { + val spStr = SpannableString(it.title) + + try { + spStr.setSpan( + ForegroundColorSpan( + Color.parseColor("#${it.color}") + ), + it.title.indexOf(it.coloredTitle), + it.title.indexOf(it.coloredTitle) + it.coloredTitle.length, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + + spStr + } catch (e: IllegalArgumentException) { + it.title + } + } else { + it.title + } + + rankCreatorAdapter.addItems(it.creators) + if (rankCreatorAdapter.itemCount <= 0 && it.creators.isEmpty()) { + binding.llCreatorRank.visibility = View.GONE + binding.rvCreatorRank.visibility = View.GONE + } else { + binding.llCreatorRank.visibility = View.VISIBLE + binding.rvCreatorRank.visibility = View.VISIBLE + } + } + } + + private fun setupRankSeries() { + rankSeriesAdapter = UserProfileSeriesListAdapter( + onClickItem = { + startActivity( + Intent(requireContext(), SeriesDetailActivity::class.java).apply { + putExtra(Constants.EXTRA_SERIES_ID, it) + } + ) + }, + onClickCreator = { + startActivity( + Intent(requireContext(), UserProfileActivity::class.java).apply { + putExtra(Constants.EXTRA_USER_ID, it) + } + ) + }, + isVisibleCreator = true + ) + + val recyclerView = binding.rvRankSeries + recyclerView.layoutManager = LinearLayoutManager( + requireContext(), + LinearLayoutManager.HORIZONTAL, + false + ) + + recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() { + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + super.getItemOffsets(outRect, view, parent, state) + + when (parent.getChildAdapterPosition(view)) { + 0 -> { + outRect.left = 0 + outRect.right = 6.7f.dpToPx().toInt() + } + + rankSeriesAdapter.itemCount - 1 -> { + outRect.right = 0 + outRect.left = 6.7f.dpToPx().toInt() + } + + else -> { + outRect.left = 6.7f.dpToPx().toInt() + outRect.right = 6.7f.dpToPx().toInt() + } + } + } + }) + + recyclerView.adapter = rankSeriesAdapter + + viewModel.rankSeriesLiveData.observe(viewLifecycleOwner) { + rankSeriesAdapter.addItems(it) + binding.llRankSeries.visibility = if ( + rankSeriesAdapter.itemCount <= 0 && it.isEmpty() + ) { + View.GONE + } else { + View.VISIBLE + } + } + } + + private fun setupRankContentSortType() { + rankContentSortAdapter = AudioContentMainNewContentThemeAdapter { + viewModel.getContentRanking(sort = it) + } + + binding.rvRankContentSort.layoutManager = LinearLayoutManager( + context, + LinearLayoutManager.HORIZONTAL, + false + ) + + binding.rvRankContentSort.addItemDecoration(object : RecyclerView.ItemDecoration() { + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + super.getItemOffsets(outRect, view, parent, state) + + when (parent.getChildAdapterPosition(view)) { + 0 -> { + outRect.left = 0 + outRect.right = 4f.dpToPx().toInt() + } + + rankContentSortAdapter.itemCount - 1 -> { + outRect.left = 4f.dpToPx().toInt() + outRect.right = 0 + } + + else -> { + outRect.left = 4f.dpToPx().toInt() + outRect.right = 4f.dpToPx().toInt() + } + } + } + }) + + binding.rvRankContentSort.adapter = rankContentSortAdapter + + viewModel.rankContentSortListLiveData.observe(viewLifecycleOwner) { + binding.llRankContent.visibility = View.VISIBLE + rankContentSortAdapter.addItems(it) + } + } + + private fun setupRankContent() { + binding.ivRankContentAll.setOnClickListener { + startActivity(Intent(requireContext(), AudioContentRankingAllActivity::class.java)) + } + + rankContentAdapter = AudioContentMainRankingAdapter( + width = (screenWidth * 0.66).toInt() + ) { + startActivity( + Intent(requireContext(), AudioContentDetailActivity::class.java).apply { + putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it) + } + ) + } + + binding.rvRankContent.layoutManager = GridLayoutManager( + context, + 3, + GridLayoutManager.HORIZONTAL, + false + ) + + binding.rvRankContent.addItemDecoration(object : RecyclerView.ItemDecoration() { + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + super.getItemOffsets(outRect, view, parent, state) + outRect.top = 13.3f.dpToPx().toInt() + outRect.bottom = 13.3f.dpToPx().toInt() + outRect.left = 13.3f.dpToPx().toInt() + outRect.right = 13.3f.dpToPx().toInt() + } + }) + + binding.rvRankContent.adapter = rankContentAdapter + + viewModel.rankContentLiveData.observe(viewLifecycleOwner) { + binding.llUploadContent.visibility = View.VISIBLE + rankContentAdapter.addItems(it) + } + } + + private fun setupEventBanner() { + val imageSliderLp = binding.eventBannerSlider.layoutParams + imageSliderLp.width = screenWidth + imageSliderLp.height = (screenWidth * 300) / 1000 + binding.eventBannerSlider.layoutParams = imageSliderLp + + binding.eventBannerSlider.apply { + adapter = EventBannerAdapter(requireContext()) { + if (it.detailImageUrl != null) { + val intent = Intent(requireActivity(), EventDetailActivity::class.java) + intent.putExtra(Constants.EXTRA_EVENT, it) + startActivity(intent) + } else if (!it.link.isNullOrBlank()) { + startActivity( + Intent( + Intent.ACTION_VIEW, + Uri.parse(it.link) + ) + ) + } + } as BaseBannerAdapter + setLifecycleRegistry(lifecycle) + setScrollDuration(800) + }.create() + + binding.eventBannerSlider + .setIndicatorView(binding.indicatorEventBanner) + .setIndicatorStyle(IndicatorStyle.ROUND_RECT) + .setIndicatorSlideMode(IndicatorSlideMode.SMOOTH) + .setIndicatorVisibility(View.GONE) + .setIndicatorSliderColor( + ContextCompat.getColor(requireContext(), R.color.color_909090), + ContextCompat.getColor(requireContext(), R.color.color_3bb9f1) + ) + .setIndicatorSliderWidth(4f.dpToPx().toInt(), 10f.dpToPx().toInt()) + .setIndicatorHeight(4f.dpToPx().toInt()) + + viewModel.eventLiveData.observe(viewLifecycleOwner) { + if (it.isNotEmpty()) { + binding.eventBannerSlider.visibility = View.VISIBLE + binding.eventBannerSlider.refreshData(it) + } else { + binding.eventBannerSlider.visibility = View.GONE + } + } + } + + private fun setupPopularContentByCreator() { + contentRankCreatorAdapter = ContentRankCreatorAdapter { + loadingPopularContentByCreator() + viewModel.getPopularContentByCreator(it) + } + + binding.rvRankingCreator.layoutManager = LinearLayoutManager( + context, + LinearLayoutManager.HORIZONTAL, + false + ) + + binding.rvRankingCreator.addItemDecoration(object : RecyclerView.ItemDecoration() { + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + super.getItemOffsets(outRect, view, parent, state) + + when (parent.getChildAdapterPosition(view)) { + 0 -> { + outRect.left = 0 + outRect.right = 4f.dpToPx().toInt() + } + + contentRankCreatorAdapter.itemCount - 1 -> { + outRect.left = 4f.dpToPx().toInt() + outRect.right = 0 + } + + else -> { + outRect.left = 4f.dpToPx().toInt() + outRect.right = 4f.dpToPx().toInt() + } + } + } + }) + + binding.rvRankingCreator.adapter = contentRankCreatorAdapter + + viewModel.contentRankCreatorListLiveData.observe(viewLifecycleOwner) { + contentRankCreatorAdapter.addItems(it) + if (rankCreatorAdapter.itemCount <= 0 && it.isEmpty()) { + binding.llCreatorContentRanking.visibility = View.GONE + } else { + binding.llCreatorContentRanking.visibility = View.VISIBLE + } + } + } + + private fun loadingPopularContentByCreator() { + binding.llSales2.visibility = View.GONE + binding.llSalesTop2.visibility = View.GONE + binding.llSalesCount2.visibility = View.GONE + binding.llSalesCountTop2.visibility = View.GONE + binding.llNoItems.visibility = View.VISIBLE + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/v2/home/AudioContentMainTabHomeRepository.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/v2/home/AudioContentMainTabHomeRepository.kt new file mode 100644 index 0000000..74f74ae --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/v2/home/AudioContentMainTabHomeRepository.kt @@ -0,0 +1,11 @@ +package kr.co.vividnext.sodalive.audio_content.main.v2.home + +import kr.co.vividnext.sodalive.audio_content.AudioContentApi + +class AudioContentMainTabHomeRepository(private val api: AudioContentApi) { + fun getContentMainHome(token: String) = api.getContentMainHome(authHeader = token) + fun getPopularContentByCreator( + creatorId: Long, + token: String + ) = api.getPopularContentByCreator(creatorId, authHeader = token) +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/v2/home/AudioContentMainTabHomeViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/v2/home/AudioContentMainTabHomeViewModel.kt new file mode 100644 index 0000000..5012a7b --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/v2/home/AudioContentMainTabHomeViewModel.kt @@ -0,0 +1,189 @@ +package kr.co.vividnext.sodalive.audio_content.main.v2.home + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.orhanobut.logger.Logger +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.schedulers.Schedulers +import kr.co.vividnext.sodalive.audio_content.AudioContentRepository +import kr.co.vividnext.sodalive.audio_content.main.ContentCreatorResponse +import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentBannerResponse +import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentRankingItem +import kr.co.vividnext.sodalive.audio_content.series.GetSeriesListResponse +import kr.co.vividnext.sodalive.base.BaseViewModel +import kr.co.vividnext.sodalive.common.SharedPreferenceManager +import kr.co.vividnext.sodalive.explorer.GetExplorerSectionResponse +import kr.co.vividnext.sodalive.settings.event.EventItem +import kr.co.vividnext.sodalive.settings.notice.NoticeItem + +class AudioContentMainTabHomeViewModel( + private val repository: AudioContentMainTabHomeRepository, + private val contentRepository: AudioContentRepository +) : BaseViewModel() { + private val _toastLiveData = MutableLiveData() + val toastLiveData: LiveData + get() = _toastLiveData + + private var _isLoading = MutableLiveData(false) + val isLoading: LiveData + get() = _isLoading + + private var _noticeLiveData = MutableLiveData() + val noticeLiveData: LiveData + get() = _noticeLiveData + + private var _contentBannerLiveData = MutableLiveData>() + val contentBannerLiveData: LiveData> + get() = _contentBannerLiveData + + private val _rankCreatorLiveData = MutableLiveData() + val rankCreatorLiveData: LiveData + get() = _rankCreatorLiveData + + private var _rankSeriesLiveData = MutableLiveData>() + val rankSeriesLiveData: LiveData> + get() = _rankSeriesLiveData + + private var _rankContentSortListLiveData = MutableLiveData>() + val rankContentSortListLiveData: LiveData> + get() = _rankContentSortListLiveData + + private var _rankContentLiveData = MutableLiveData>() + val rankContentLiveData: LiveData> + get() = _rankContentLiveData + + private val _eventLiveData = MutableLiveData>() + val eventLiveData: LiveData> + get() = _eventLiveData + + private val _contentRankCreatorListLiveData = MutableLiveData>() + val contentRankCreatorListLiveData: LiveData> + get() = _contentRankCreatorListLiveData + + private val _salesRankContentListLiveData = MutableLiveData>() + val salesRankContentListLiveData: LiveData> + get() = _salesRankContentListLiveData + + private val _salesCountRankContentListLiveData = + MutableLiveData>() + val salesCountRankContentListLiveData: LiveData> + get() = _salesCountRankContentListLiveData + + fun fetchData() { + _isLoading.value = true + compositeDisposable.add( + repository.getContentMainHome(token = "Bearer ${SharedPreferenceManager.token}") + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + if (it.success && it.data != null) { + val data = it.data + + Logger.e("data: $data") + + if (data.latestNotice != null) { + _noticeLiveData.value = data.latestNotice!! + } + _contentBannerLiveData.value = data.bannerList + _rankCreatorLiveData.value = data.rankCreatorList + _rankSeriesLiveData.value = data.rankSeriesList + _rankContentLiveData.value = data.rankContentList + _rankContentSortListLiveData.value = data.rankSortTypeList + _eventLiveData.value = data.eventBannerList.eventList + _contentRankCreatorListLiveData.value = data.contentRankCreatorList + _salesRankContentListLiveData.value = data.salesRankContentList + _salesCountRankContentListLiveData.value = + data.salesCountRankContentList + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + } + + _isLoading.value = false + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } + + fun getContentRanking(sort: String = "매출") { + _isLoading.value = true + compositeDisposable.add( + contentRepository.getContentRanking( + page = 1, + size = 12, + sortType = sort, + token = "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + _isLoading.value = false + if (it.success && it.data != null) { + _rankContentLiveData.value = it.data.items + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + } + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } + + fun getPopularContentByCreator(creatorId: Long) { + _isLoading.value = true + compositeDisposable.add( + repository.getPopularContentByCreator( + creatorId = creatorId, + token = "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + _isLoading.value = false + if (it.success && it.data != null) { + val data = it.data + _salesRankContentListLiveData.value = data.salesRankContentList + _salesCountRankContentListLiveData.value = + data.salesCountRankContentList + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + } + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/v2/home/GetContentMainTabHomeResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/v2/home/GetContentMainTabHomeResponse.kt new file mode 100644 index 0000000..f21efac --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/v2/home/GetContentMainTabHomeResponse.kt @@ -0,0 +1,24 @@ +package kr.co.vividnext.sodalive.audio_content.main.v2.home + +import androidx.annotation.Keep +import kr.co.vividnext.sodalive.audio_content.main.ContentCreatorResponse +import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentBannerResponse +import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentRankingItem +import kr.co.vividnext.sodalive.audio_content.series.GetSeriesListResponse +import kr.co.vividnext.sodalive.explorer.GetExplorerSectionResponse +import kr.co.vividnext.sodalive.settings.event.GetEventResponse +import kr.co.vividnext.sodalive.settings.notice.NoticeItem + +@Keep +data class GetContentMainTabHomeResponse( + val latestNotice: NoticeItem?, + val bannerList: List, + val rankCreatorList: GetExplorerSectionResponse, + val rankSeriesList: List, + val rankSortTypeList: List, + val rankContentList: List, + val eventBannerList: GetEventResponse, + val contentRankCreatorList: List, + val salesRankContentList: List, + val salesCountRankContentList: List +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt b/app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt index 7b043f3..6f7b3ce 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt @@ -23,6 +23,8 @@ import kr.co.vividnext.sodalive.audio_content.main.order.AudioContentMainOrderLi import kr.co.vividnext.sodalive.audio_content.main.ranking.AudioContentMainCreatorRankingViewModel import kr.co.vividnext.sodalive.audio_content.main.ranking.AudioContentMainRankingViewModel import kr.co.vividnext.sodalive.audio_content.main.recommend_series.AudioContentMainRecommendSeriesViewModel +import kr.co.vividnext.sodalive.audio_content.main.v2.home.AudioContentMainTabHomeRepository +import kr.co.vividnext.sodalive.audio_content.main.v2.home.AudioContentMainTabHomeViewModel import kr.co.vividnext.sodalive.audio_content.modify.AudioContentModifyViewModel import kr.co.vividnext.sodalive.audio_content.order.AudioContentOrderListViewModel import kr.co.vividnext.sodalive.audio_content.player.AudioContentGenerateUrlRepository @@ -290,6 +292,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) { viewModel { AuditionDetailViewModel(get()) } viewModel { AuditionRoleDetailViewModel(get()) } viewModel { AudioContentMainCreatorRankingViewModel(get()) } + viewModel { AudioContentMainTabHomeViewModel(get(), get()) } } private val repositoryModule = module { @@ -321,6 +324,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) { factory { AudioContentPlaylistRepository(get()) } factory { AudioContentGenerateUrlRepository(get()) } factory { AuditionRepository(get()) } + factory { AudioContentMainTabHomeRepository(get()) } } private val moduleList = listOf( diff --git a/app/src/main/java/kr/co/vividnext/sodalive/main/MainActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/main/MainActivity.kt index 6f877ad..f07c691 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/main/MainActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/main/MainActivity.kt @@ -1,6 +1,7 @@ package kr.co.vividnext.sodalive.main import android.Manifest +import android.annotation.SuppressLint import android.app.Service import android.content.BroadcastReceiver import android.content.ComponentName @@ -39,7 +40,7 @@ import io.reactivex.rxjava3.schedulers.Schedulers import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.audio_content.AudioContentPlayService import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity -import kr.co.vividnext.sodalive.audio_content.main.AudioContentMainFragment +import kr.co.vividnext.sodalive.audio_content.main.v2.home.AudioContentMainTabHomeFragment import kr.co.vividnext.sodalive.audio_content.player.AudioContentPlayerFragment import kr.co.vividnext.sodalive.audio_content.player.AudioContentPlayerService import kr.co.vividnext.sodalive.audition.AuditionFragment @@ -232,6 +233,7 @@ class MainActivity : BaseActivity(ActivityMainBinding::infl playerFragment.show(supportFragmentManager, playerFragment.tag) } + @SuppressLint("UnspecifiedRegisterReceiverFlag") override fun onResume() { super.onResume() val intentFilter = IntentFilter(Constants.ACTION_MAIN_AUDIO_CONTENT_RECEIVER) @@ -445,7 +447,7 @@ class MainActivity : BaseActivity(ActivityMainBinding::infl if (fragment == null) { fragment = when (currentTab) { MainViewModel.CurrentTab.LIVE -> liveFragment - MainViewModel.CurrentTab.CONTENT -> AudioContentMainFragment() + MainViewModel.CurrentTab.CONTENT -> AudioContentMainTabHomeFragment() MainViewModel.CurrentTab.AUDITION -> AuditionFragment() MainViewModel.CurrentTab.MESSAGE -> MessageFragment() MainViewModel.CurrentTab.MY -> MyPageFragment() @@ -528,6 +530,7 @@ class MainActivity : BaseActivity(ActivityMainBinding::infl } } + @SuppressLint("NotifyDataSetChanged") private fun setupSearchChannelView() { searchChannelAdapter = SelectMessageRecipientAdapter { hideKeyboard() diff --git a/app/src/main/res/drawable-xxhdpi/ic_category_alarm.png b/app/src/main/res/drawable-xxhdpi/ic_category_alarm.png new file mode 100644 index 0000000..65c1ed1 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_category_alarm.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_category_asmr.png b/app/src/main/res/drawable-xxhdpi/ic_category_asmr.png new file mode 100644 index 0000000..f411f57 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_category_asmr.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_category_audio_book.png b/app/src/main/res/drawable-xxhdpi/ic_category_audio_book.png new file mode 100644 index 0000000..4c107a3 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_category_audio_book.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_category_audio_toon.png b/app/src/main/res/drawable-xxhdpi/ic_category_audio_toon.png new file mode 100644 index 0000000..55aac47 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_category_audio_toon.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_category_content.png b/app/src/main/res/drawable-xxhdpi/ic_category_content.png new file mode 100644 index 0000000..98eacd4 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_category_content.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_category_free.png b/app/src/main/res/drawable-xxhdpi/ic_category_free.png new file mode 100644 index 0000000..1085485 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_category_free.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_category_replay.png b/app/src/main/res/drawable-xxhdpi/ic_category_replay.png new file mode 100644 index 0000000..9309716 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_category_replay.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_category_series.png b/app/src/main/res/drawable-xxhdpi/ic_category_series.png new file mode 100644 index 0000000..c5d7724 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_category_series.png differ diff --git a/app/src/main/res/layout/fragment_audio_content_main_tab_home.xml b/app/src/main/res/layout/fragment_audio_content_main_tab_home.xml new file mode 100644 index 0000000..52e7942 --- /dev/null +++ b/app/src/main/res/layout/fragment_audio_content_main_tab_home.xml @@ -0,0 +1,872 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_content_rank_creator.xml b/app/src/main/res/layout/item_content_rank_creator.xml new file mode 100644 index 0000000..e0da79e --- /dev/null +++ b/app/src/main/res/layout/item_content_rank_creator.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + +