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 9cb4d7c..0326e8d 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 @@ -21,6 +21,7 @@ import kr.co.vividnext.sodalive.audio_content.main.v2.alarm.GetContentMainTabAla import kr.co.vividnext.sodalive.audio_content.main.v2.asmr.GetContentMainTabAsmrResponse import kr.co.vividnext.sodalive.audio_content.main.v2.content.GetContentMainTabContentResponse import kr.co.vividnext.sodalive.audio_content.main.v2.home.GetContentMainTabHomeResponse +import kr.co.vividnext.sodalive.audio_content.main.v2.replay.GetContentMainTabLiveReplayResponse import kr.co.vividnext.sodalive.audio_content.main.v2.series.GetContentMainTabSeriesResponse import kr.co.vividnext.sodalive.audio_content.order.GetAudioContentOrderListResponse import kr.co.vividnext.sodalive.audio_content.order.OrderRequest @@ -294,4 +295,9 @@ interface AudioContentApi { fun getContentMainAsmr( @Header("Authorization") authHeader: String ): Single> + + @GET("/v2/audio-content/main/replay") + fun getContentMainReplay( + @Header("Authorization") authHeader: String + ): Single> } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/v2/replay/AudioContentMainTabReplayFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/v2/replay/AudioContentMainTabReplayFragment.kt index fbc9426..0908439 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/v2/replay/AudioContentMainTabReplayFragment.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/v2/replay/AudioContentMainTabReplayFragment.kt @@ -1,9 +1,386 @@ package kr.co.vividnext.sodalive.audio_content.main.v2.replay +import android.annotation.SuppressLint +import android.content.Intent +import android.graphics.Rect +import android.net.Uri +import android.os.Bundle +import android.view.View +import android.widget.LinearLayout +import android.widget.Toast +import androidx.annotation.OptIn +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 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.detail.AudioContentDetailActivity +import kr.co.vividnext.sodalive.audio_content.main.AudioContentBannerType +import kr.co.vividnext.sodalive.audio_content.main.AudioContentMainContentAdapter +import kr.co.vividnext.sodalive.audio_content.main.banner.AudioContentMainBannerAdapter +import kr.co.vividnext.sodalive.audio_content.main.ranking.AudioContentMainRankingAdapter +import kr.co.vividnext.sodalive.audio_content.main.v2.AudioContentMainContentCurationAdapter +import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity 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.databinding.FragmentAudioContentMainTabReplayBinding +import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity +import kr.co.vividnext.sodalive.extensions.dpToPx +import kr.co.vividnext.sodalive.live.event_banner.EventBannerAdapter +import kr.co.vividnext.sodalive.settings.event.EventDetailActivity +import org.koin.android.ext.android.inject +import kotlin.math.roundToInt +@OptIn(UnstableApi::class) class AudioContentMainTabReplayFragment : BaseFragment( FragmentAudioContentMainTabReplayBinding::inflate ) { + private val viewModel: AudioContentMainTabReplayViewModel by inject() + + private lateinit var loadingDialog: LoadingDialog + private lateinit var contentBannerAdapter: AudioContentMainBannerAdapter + private lateinit var newContentAdapter: AudioContentMainContentAdapter + private lateinit var contentRankingAdapter: AudioContentMainRankingAdapter + private lateinit var curationAdapter: AudioContentMainContentCurationAdapter + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupView() + bindData() + + viewModel.fetchData() + } + + private fun setupView() { + loadingDialog = LoadingDialog(requireActivity(), layoutInflater) + + setupContentBanner() + setupNewContent() + setupContentRanking() + setupEventBanner() + setupCuration() + } + + 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 setupNewContent() { + binding.ivNewContentAll.setOnClickListener {} + + newContentAdapter = AudioContentMainContentAdapter( + onClickItem = { + startActivity( + Intent(requireContext(), AudioContentDetailActivity::class.java).apply { + putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it) + } + ) + }, + onClickCreator = { + startActivity( + Intent(requireContext(), UserProfileActivity::class.java).apply { + putExtra(Constants.EXTRA_USER_ID, it) + } + ) + } + ) + + binding.rvNewContent.layoutManager = LinearLayoutManager( + context, + LinearLayoutManager.HORIZONTAL, + false + ) + + binding.rvNewContent.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() + } + + newContentAdapter.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.rvNewContent.adapter = newContentAdapter + + viewModel.newContentListLiveData.observe(viewLifecycleOwner) { + newContentAdapter.addItems(it) + } + } + + @SuppressLint("SetTextI18n") + private fun setupContentRanking() { + contentRankingAdapter = AudioContentMainRankingAdapter( + width = (screenWidth * 0.66).toInt() + ) { + startActivity( + Intent(requireContext(), AudioContentDetailActivity::class.java).apply { + putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it) + } + ) + } + + binding.rvContentRanking.layoutManager = GridLayoutManager( + context, + 3, + GridLayoutManager.HORIZONTAL, + false + ) + + binding.rvContentRanking.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.rvContentRanking.adapter = contentRankingAdapter + + viewModel.contentRankingLiveData.observe(viewLifecycleOwner) { + if (it.isNotEmpty()) { + binding.llContentRanking.visibility = View.VISIBLE + contentRankingAdapter.addItems(it) + } else { + binding.llContentRanking.visibility = View.GONE + } + } + } + + 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.indicatorEventBanner.visibility = View.VISIBLE + binding.eventBannerSlider.refreshData(it) + } else { + binding.eventBannerSlider.visibility = View.GONE + binding.indicatorEventBanner.visibility = View.GONE + } + } + } + + private fun setupCuration() { + curationAdapter = AudioContentMainContentCurationAdapter( + onClickItem = { + startActivity( + Intent(requireContext(), AudioContentDetailActivity::class.java).apply { + putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it) + } + ) + }, + onClickCreator = { + startActivity( + Intent(requireContext(), UserProfileActivity::class.java).apply { + putExtra(Constants.EXTRA_USER_ID, it) + } + ) + } + ) + + binding.rvCuration.layoutManager = LinearLayoutManager( + context, + LinearLayoutManager.VERTICAL, + false + ) + + binding.rvCuration.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 = 30f.dpToPx().toInt() + outRect.bottom = 15f.dpToPx().toInt() + } + + curationAdapter.itemCount - 1 -> { + outRect.top = 15f.dpToPx().toInt() + outRect.bottom = 30f.dpToPx().toInt() + } + + else -> { + outRect.top = 15f.dpToPx().toInt() + outRect.bottom = 15f.dpToPx().toInt() + } + } + } + }) + + binding.rvCuration.adapter = curationAdapter + + viewModel.curationListLiveData.observe(viewLifecycleOwner) { + curationAdapter.addItems(it) + + binding.rvCuration.visibility = if (curationAdapter.itemCount <= 0 && it.isEmpty()) { + View.GONE + } else { + View.VISIBLE + } + } + } + + 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() + } + } + } } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/v2/replay/AudioContentMainTabReplayRepository.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/v2/replay/AudioContentMainTabReplayRepository.kt new file mode 100644 index 0000000..ff1509c --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/v2/replay/AudioContentMainTabReplayRepository.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.audio_content.main.v2.replay + +import kr.co.vividnext.sodalive.audio_content.AudioContentApi + +class AudioContentMainTabReplayRepository(private val api: AudioContentApi) { + fun getContentMainReplay(token: String) = api.getContentMainReplay(authHeader = token) +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/v2/replay/AudioContentMainTabReplayViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/v2/replay/AudioContentMainTabReplayViewModel.kt new file mode 100644 index 0000000..dfb8686 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/v2/replay/AudioContentMainTabReplayViewModel.kt @@ -0,0 +1,84 @@ +package kr.co.vividnext.sodalive.audio_content.main.v2.replay + +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.main.GetAudioContentBannerResponse +import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem +import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentRankingItem +import kr.co.vividnext.sodalive.audio_content.main.v2.GetContentCurationResponse +import kr.co.vividnext.sodalive.base.BaseViewModel +import kr.co.vividnext.sodalive.common.SharedPreferenceManager +import kr.co.vividnext.sodalive.settings.event.EventItem + +class AudioContentMainTabReplayViewModel( + private val repository: AudioContentMainTabReplayRepository +): BaseViewModel() { + private val _toastLiveData = MutableLiveData() + val toastLiveData: LiveData + get() = _toastLiveData + + private var _isLoading = MutableLiveData(false) + val isLoading: LiveData + get() = _isLoading + + private var _contentBannerLiveData = MutableLiveData>() + val contentBannerLiveData: LiveData> + get() = _contentBannerLiveData + + private var _newContentListLiveData = MutableLiveData>() + val newContentListLiveData: LiveData> + get() = _newContentListLiveData + + private var _contentRankingLiveData = MutableLiveData>() + val contentRankingLiveData: LiveData> + get() = _contentRankingLiveData + + private val _eventLiveData = MutableLiveData>() + val eventLiveData: LiveData> + get() = _eventLiveData + + private var _curationListLiveData = MutableLiveData>() + val curationListLiveData: LiveData> + get() = _curationListLiveData + + fun fetchData() { + _isLoading.value = true + compositeDisposable.add( + repository.getContentMainReplay(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") + _contentBannerLiveData.value = data.contentBannerList + _newContentListLiveData.value = data.newLiveReplayContentList + _contentRankingLiveData.value = data.rankLiveReplayContentList + _eventLiveData.value = data.eventBannerList.eventList + _curationListLiveData.value = data.curationList + } 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("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/v2/replay/GetContentMainTabLiveReplayResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/v2/replay/GetContentMainTabLiveReplayResponse.kt new file mode 100644 index 0000000..c23f534 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/v2/replay/GetContentMainTabLiveReplayResponse.kt @@ -0,0 +1,18 @@ +package kr.co.vividnext.sodalive.audio_content.main.v2.replay + +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName +import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentBannerResponse +import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem +import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentRankingItem +import kr.co.vividnext.sodalive.audio_content.main.v2.GetContentCurationResponse +import kr.co.vividnext.sodalive.settings.event.GetEventResponse + +@Keep +data class GetContentMainTabLiveReplayResponse( + @SerializedName("contentBannerList") val contentBannerList: List, + @SerializedName("newLiveReplayContentList") val newLiveReplayContentList: List, + @SerializedName("rankLiveReplayContentList") val rankLiveReplayContentList: List, + @SerializedName("eventBannerList") val eventBannerList: GetEventResponse, + @SerializedName("curationList") val curationList: 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 a9d5488..8b95ab3 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 @@ -31,6 +31,8 @@ import kr.co.vividnext.sodalive.audio_content.main.v2.content.AudioContentMainTa import kr.co.vividnext.sodalive.audio_content.main.v2.content.AudioContentMainTabContentViewModel 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.main.v2.replay.AudioContentMainTabReplayRepository +import kr.co.vividnext.sodalive.audio_content.main.v2.replay.AudioContentMainTabReplayViewModel import kr.co.vividnext.sodalive.audio_content.main.v2.series.AudioContentMainTabSeriesRepository import kr.co.vividnext.sodalive.audio_content.main.v2.series.AudioContentMainTabSeriesViewModel import kr.co.vividnext.sodalive.audio_content.modify.AudioContentModifyViewModel @@ -305,6 +307,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) { viewModel { AudioContentMainTabContentViewModel(get()) } viewModel { AudioContentMainTabAlarmViewModel(get()) } viewModel { AudioContentMainTabAsmrViewModel(get()) } + viewModel { AudioContentMainTabReplayViewModel(get()) } } private val repositoryModule = module { @@ -341,6 +344,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) { factory { AudioContentMainTabContentRepository(get()) } factory { AudioContentMainTabAlarmRepository(get()) } factory { AudioContentMainTabAsmrRepository(get()) } + factory { AudioContentMainTabReplayRepository(get()) } } private val moduleList = listOf( diff --git a/app/src/main/res/layout/fragment_audio_content_main_tab_replay.xml b/app/src/main/res/layout/fragment_audio_content_main_tab_replay.xml index 1b31835..e96df64 100644 --- a/app/src/main/res/layout/fragment_audio_content_main_tab_replay.xml +++ b/app/src/main/res/layout/fragment_audio_content_main_tab_replay.xml @@ -21,5 +21,92 @@ android:layout_height="wrap_content" android:layout_gravity="center" android:layout_marginTop="6.7dp" /> + + + + + + + + + + + + + + + + + + + + + + + + +