feat(content): 전체 탭 화면 연결을 추가한다
This commit is contained in:
@@ -2,9 +2,15 @@ package kr.co.vividnext.sodalive.v2.main.content
|
|||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.Gravity
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import kr.co.vividnext.sodalive.R
|
import kr.co.vividnext.sodalive.R
|
||||||
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
|
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
|
||||||
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
|
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
|
||||||
@@ -14,7 +20,13 @@ import kr.co.vividnext.sodalive.common.LoadingDialog
|
|||||||
import kr.co.vividnext.sodalive.common.ToastMessage
|
import kr.co.vividnext.sodalive.common.ToastMessage
|
||||||
import kr.co.vividnext.sodalive.databinding.FragmentV2MainContentBinding
|
import kr.co.vividnext.sodalive.databinding.FragmentV2MainContentBinding
|
||||||
import kr.co.vividnext.sodalive.databinding.ViewSectionTitleBinding
|
import kr.co.vividnext.sodalive.databinding.ViewSectionTitleBinding
|
||||||
|
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
||||||
|
import kr.co.vividnext.sodalive.home.SeriesPublishedDaysOfWeek
|
||||||
|
import kr.co.vividnext.sodalive.v2.common.data.ContentSort
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.model.toLabelResId
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.ui.CreatorChannelSortPopup
|
||||||
import kr.co.vividnext.sodalive.v2.main.content.data.AudioRankingType
|
import kr.co.vividnext.sodalive.v2.main.content.data.AudioRankingType
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.content.data.MainContentAllType
|
||||||
import kr.co.vividnext.sodalive.v2.main.content.model.AudioRankingsUiState
|
import kr.co.vividnext.sodalive.v2.main.content.model.AudioRankingsUiState
|
||||||
import kr.co.vividnext.sodalive.v2.main.content.model.AudioRecommendationsUiState
|
import kr.co.vividnext.sodalive.v2.main.content.model.AudioRecommendationsUiState
|
||||||
import kr.co.vividnext.sodalive.v2.main.content.model.ContentAudioCardSection
|
import kr.co.vividnext.sodalive.v2.main.content.model.ContentAudioCardSection
|
||||||
@@ -25,9 +37,17 @@ import kr.co.vividnext.sodalive.v2.main.content.model.ContentCommentedAudioSecti
|
|||||||
import kr.co.vividnext.sodalive.v2.main.content.model.ContentCommentedAudioUiModel
|
import kr.co.vividnext.sodalive.v2.main.content.model.ContentCommentedAudioUiModel
|
||||||
import kr.co.vividnext.sodalive.v2.main.content.model.ContentOriginalSeriesSection
|
import kr.co.vividnext.sodalive.v2.main.content.model.ContentOriginalSeriesSection
|
||||||
import kr.co.vividnext.sodalive.v2.main.content.model.ContentOriginalSeriesUiModel
|
import kr.co.vividnext.sodalive.v2.main.content.model.ContentOriginalSeriesUiModel
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.content.model.MainContentAllTabUiState
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.content.model.contentAllDayOfWeekOptions
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.content.model.toContentAllDayLabelResId
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.content.model.toContentAllTypeLabelResId
|
||||||
import kr.co.vividnext.sodalive.v2.main.content.model.toContentBannerIntent
|
import kr.co.vividnext.sodalive.v2.main.content.model.toContentBannerIntent
|
||||||
import kr.co.vividnext.sodalive.v2.main.content.model.toContentBannerRoute
|
import kr.co.vividnext.sodalive.v2.main.content.model.toContentBannerRoute
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.content.model.usesDayOfWeekQuery
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.content.model.usesSeriesItems
|
||||||
import kr.co.vividnext.sodalive.v2.main.content.ui.CONTENT_RECOMMENDED_GRID_SPAN_COUNT
|
import kr.co.vividnext.sodalive.v2.main.content.ui.CONTENT_RECOMMENDED_GRID_SPAN_COUNT
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.content.ui.ContentAllAudioCardAdapter
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.content.ui.ContentAllSeriesCardAdapter
|
||||||
import kr.co.vividnext.sodalive.v2.main.content.ui.ContentAudioCardAdapter
|
import kr.co.vividnext.sodalive.v2.main.content.ui.ContentAudioCardAdapter
|
||||||
import kr.co.vividnext.sodalive.v2.main.content.ui.ContentBannerBinder
|
import kr.co.vividnext.sodalive.v2.main.content.ui.ContentBannerBinder
|
||||||
import kr.co.vividnext.sodalive.v2.main.content.ui.ContentCommentedAudioAdapter
|
import kr.co.vividnext.sodalive.v2.main.content.ui.ContentCommentedAudioAdapter
|
||||||
@@ -45,7 +65,15 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
|||||||
) {
|
) {
|
||||||
private val contentMainViewModel: ContentMainViewModel by viewModel()
|
private val contentMainViewModel: ContentMainViewModel by viewModel()
|
||||||
private val contentRankingViewModel: ContentRankingViewModel by viewModel()
|
private val contentRankingViewModel: ContentRankingViewModel by viewModel()
|
||||||
|
private val contentAllTabViewModel: ContentAllTabViewModel by viewModel()
|
||||||
private val loadingDialog: LoadingDialog by lazy { LoadingDialog(requireActivity(), layoutInflater) }
|
private val loadingDialog: LoadingDialog by lazy { LoadingDialog(requireActivity(), layoutInflater) }
|
||||||
|
private val contentAllTypes = listOf(
|
||||||
|
MainContentAllType.AUDIO,
|
||||||
|
MainContentAllType.SERIES,
|
||||||
|
MainContentAllType.ORIGINAL,
|
||||||
|
MainContentAllType.FREE,
|
||||||
|
MainContentAllType.POINT
|
||||||
|
)
|
||||||
private val originalSeriesAdapter = ContentOriginalSeriesAdapter { openSeriesDetail(it) }
|
private val originalSeriesAdapter = ContentOriginalSeriesAdapter { openSeriesDetail(it) }
|
||||||
private val latestAudioAdapter = ContentAudioCardAdapter(AudioContentCardSize.Medium) { openAudioContentDetail(it) }
|
private val latestAudioAdapter = ContentAudioCardAdapter(AudioContentCardSize.Medium) { openAudioContentDetail(it) }
|
||||||
private val newAndHotAdapter = ContentNewAndHotAdapter { openAudioContentDetail(it) }
|
private val newAndHotAdapter = ContentNewAndHotAdapter { openAudioContentDetail(it) }
|
||||||
@@ -54,15 +82,27 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
|||||||
private val commentedAudioAdapter = ContentCommentedAudioAdapter { openAudioContentDetail(it) }
|
private val commentedAudioAdapter = ContentCommentedAudioAdapter { openAudioContentDetail(it) }
|
||||||
private val recommendedAudioAdapter = ContentAudioCardAdapter(AudioContentCardSize.Large) { openAudioContentDetail(it) }
|
private val recommendedAudioAdapter = ContentAudioCardAdapter(AudioContentCardSize.Large) { openAudioContentDetail(it) }
|
||||||
private val contentRankingAdapter = ContentRankingAdapter { openRankingAudioContentDetail(it) }
|
private val contentRankingAdapter = ContentRankingAdapter { openRankingAudioContentDetail(it) }
|
||||||
|
private val contentAllAudioCardAdapter = ContentAllAudioCardAdapter { audioContentId ->
|
||||||
|
openAudioContentDetail(audioContentId)
|
||||||
|
}
|
||||||
|
private val contentAllSeriesCardAdapter = ContentAllSeriesCardAdapter { seriesId -> openSeriesDetail(seriesId) }
|
||||||
private var bannerBinder: ContentBannerBinder? = null
|
private var bannerBinder: ContentBannerBinder? = null
|
||||||
|
private var sortPopup: CreatorChannelSortPopup? = null
|
||||||
private var isRecommendationLoading = false
|
private var isRecommendationLoading = false
|
||||||
private var isRankingLoading = false
|
private var isRankingLoading = false
|
||||||
|
private var isAllTabLoading = false
|
||||||
private var hasSelectedRankingTab = false
|
private var hasSelectedRankingTab = false
|
||||||
|
private var hasSelectedAllTab = false
|
||||||
|
private var currentContentTab = CONTENT_TAB_RECOMMENDATION
|
||||||
|
private var currentAllTabState: MainContentAllTabUiState? = null
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
setUpTextTabs()
|
setUpTextTabs()
|
||||||
setUpRankingTypeTabs()
|
setUpRankingTypeTabs()
|
||||||
|
setUpAllTypeTabs()
|
||||||
|
setUpAllDayFilter()
|
||||||
|
setUpAllSortButton()
|
||||||
showContentTab(CONTENT_TAB_RECOMMENDATION)
|
showContentTab(CONTENT_TAB_RECOMMENDATION)
|
||||||
setUpSectionTitles()
|
setUpSectionTitles()
|
||||||
setUpAdapters()
|
setUpAdapters()
|
||||||
@@ -70,6 +110,14 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
|||||||
contentMainViewModel.loadRecommendations()
|
contentMainViewModel.loadRecommendations()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
sortPopup?.dismiss()
|
||||||
|
sortPopup = null
|
||||||
|
bannerBinder = null
|
||||||
|
binding.rvContentAllItems.adapter = null
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
private fun setUpTextTabs() {
|
private fun setUpTextTabs() {
|
||||||
binding.textTabBarContent.root.setMenus(
|
binding.textTabBarContent.root.setMenus(
|
||||||
listOf(
|
listOf(
|
||||||
@@ -94,11 +142,56 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setUpAllTypeTabs() {
|
||||||
|
binding.viewContentAllTypeTabs.root.setMenus(
|
||||||
|
contentAllTypes.map { type -> getString(type.toContentAllTypeLabelResId()) },
|
||||||
|
selectedIndex = MainContentAllType.AUDIO.ordinal
|
||||||
|
)
|
||||||
|
binding.viewContentAllTypeTabs.root.setOnTabSelectedListener { index ->
|
||||||
|
contentAllTabViewModel.changeType(contentAllTypes[index])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setUpAllDayFilter() {
|
||||||
|
binding.layoutContentAllDayFilter.removeAllViews()
|
||||||
|
contentAllDayOfWeekOptions.forEach { dayOfWeek ->
|
||||||
|
binding.layoutContentAllDayFilter.addView(createAllDayFilterView(dayOfWeek))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createAllDayFilterView(dayOfWeek: SeriesPublishedDaysOfWeek): TextView {
|
||||||
|
return TextView(requireContext()).apply {
|
||||||
|
tag = dayOfWeek
|
||||||
|
gravity = Gravity.CENTER
|
||||||
|
setText(dayOfWeek.toContentAllDayLabelResId())
|
||||||
|
setTextAppearance(R.style.Typography_Body5)
|
||||||
|
setPadding(
|
||||||
|
resources.getDimensionPixelSize(R.dimen.spacing_8),
|
||||||
|
0,
|
||||||
|
resources.getDimensionPixelSize(R.dimen.spacing_8),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
layoutParams = LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
)
|
||||||
|
setOnClickListener { contentAllTabViewModel.changeDayOfWeek(dayOfWeek) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setUpAllSortButton() {
|
||||||
|
binding.layoutContentAllSortButton.setOnClickListener {
|
||||||
|
val state = currentAllTabState ?: return@setOnClickListener
|
||||||
|
showAllSortPopup(state.selectedSort)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun showContentTab(index: Int) {
|
private fun showContentTab(index: Int) {
|
||||||
|
currentContentTab = index
|
||||||
when (index) {
|
when (index) {
|
||||||
CONTENT_TAB_RECOMMENDATION -> showRecommendationContent()
|
CONTENT_TAB_RECOMMENDATION -> showRecommendationContent()
|
||||||
CONTENT_TAB_RANKING -> showRankingContent()
|
CONTENT_TAB_RANKING -> showRankingContent()
|
||||||
CONTENT_TAB_ALL -> hideContentSurfaces()
|
CONTENT_TAB_ALL -> showAllContent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,22 +199,37 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
|||||||
binding.nsvContentRecommendationContent.visibility = View.VISIBLE
|
binding.nsvContentRecommendationContent.visibility = View.VISIBLE
|
||||||
binding.viewContentRankingTypeTabs.root.visibility = View.GONE
|
binding.viewContentRankingTypeTabs.root.visibility = View.GONE
|
||||||
binding.rvContentRankings.visibility = View.GONE
|
binding.rvContentRankings.visibility = View.GONE
|
||||||
|
binding.layoutContentAllSurface.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showRankingContent() {
|
private fun showRankingContent() {
|
||||||
binding.nsvContentRecommendationContent.visibility = View.GONE
|
binding.nsvContentRecommendationContent.visibility = View.GONE
|
||||||
binding.viewContentRankingTypeTabs.root.visibility = View.VISIBLE
|
binding.viewContentRankingTypeTabs.root.visibility = View.VISIBLE
|
||||||
binding.rvContentRankings.visibility = View.VISIBLE
|
binding.rvContentRankings.visibility = View.VISIBLE
|
||||||
|
binding.layoutContentAllSurface.visibility = View.GONE
|
||||||
if (!hasSelectedRankingTab) {
|
if (!hasSelectedRankingTab) {
|
||||||
hasSelectedRankingTab = true
|
hasSelectedRankingTab = true
|
||||||
contentRankingViewModel.loadRankings(AudioRankingType.WEEKLY_POPULAR)
|
contentRankingViewModel.loadRankings(AudioRankingType.WEEKLY_POPULAR)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showAllContent() {
|
||||||
|
binding.nsvContentRecommendationContent.visibility = View.GONE
|
||||||
|
binding.viewContentRankingTypeTabs.root.visibility = View.GONE
|
||||||
|
binding.rvContentRankings.visibility = View.GONE
|
||||||
|
binding.layoutContentAllSurface.visibility = View.VISIBLE
|
||||||
|
currentAllTabState?.let(::renderAllTabState)
|
||||||
|
if (currentAllTabState == null && !hasSelectedAllTab) {
|
||||||
|
hasSelectedAllTab = true
|
||||||
|
contentAllTabViewModel.loadInitial()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun hideContentSurfaces() {
|
private fun hideContentSurfaces() {
|
||||||
binding.nsvContentRecommendationContent.visibility = View.GONE
|
binding.nsvContentRecommendationContent.visibility = View.GONE
|
||||||
binding.viewContentRankingTypeTabs.root.visibility = View.GONE
|
binding.viewContentRankingTypeTabs.root.visibility = View.GONE
|
||||||
binding.rvContentRankings.visibility = View.GONE
|
binding.rvContentRankings.visibility = View.GONE
|
||||||
|
binding.layoutContentAllSurface.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setUpAdapters() {
|
private fun setUpAdapters() {
|
||||||
@@ -167,6 +275,24 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
|||||||
layoutManager = ContentRankingAdapter.createGridLayoutManager(requireContext())
|
layoutManager = ContentRankingAdapter.createGridLayoutManager(requireContext())
|
||||||
adapter = contentRankingAdapter
|
adapter = contentRankingAdapter
|
||||||
}
|
}
|
||||||
|
val contentAllGridLayoutManager = GridLayoutManager(requireContext(), CONTENT_ALL_GRID_SPAN_COUNT)
|
||||||
|
binding.rvContentAllItems.apply {
|
||||||
|
layoutManager = contentAllGridLayoutManager
|
||||||
|
adapter = contentAllAudioCardAdapter
|
||||||
|
addContentGridItemSpacing(CONTENT_ALL_GRID_SPAN_COUNT)
|
||||||
|
addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||||
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
|
super.onScrolled(recyclerView, dx, dy)
|
||||||
|
if (dy <= 0) return
|
||||||
|
|
||||||
|
val itemCount = recyclerView.adapter?.itemCount ?: return
|
||||||
|
val lastVisiblePosition = contentAllGridLayoutManager.findLastVisibleItemPosition()
|
||||||
|
if (lastVisiblePosition >= itemCount - CONTENT_ALL_GRID_SPAN_COUNT) {
|
||||||
|
contentAllTabViewModel.loadMore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bindObservers() {
|
private fun bindObservers() {
|
||||||
@@ -202,6 +328,116 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
|||||||
contentRankingViewModel.toastLiveData.observe(viewLifecycleOwner) { toastMessage ->
|
contentRankingViewModel.toastLiveData.observe(viewLifecycleOwner) { toastMessage ->
|
||||||
toastMessage?.let(::showToast)
|
toastMessage?.let(::showToast)
|
||||||
}
|
}
|
||||||
|
contentAllTabViewModel.allTabStateLiveData.observe(viewLifecycleOwner) { state ->
|
||||||
|
currentAllTabState = state
|
||||||
|
renderAllTabState(state)
|
||||||
|
}
|
||||||
|
contentAllTabViewModel.isLoading.observe(viewLifecycleOwner) { isLoading ->
|
||||||
|
isAllTabLoading = isLoading
|
||||||
|
updateLoadingDialog()
|
||||||
|
}
|
||||||
|
contentAllTabViewModel.toastLiveData.observe(viewLifecycleOwner) { toastMessage ->
|
||||||
|
toastMessage?.let(::showToast)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderAllTabState(state: MainContentAllTabUiState) {
|
||||||
|
if (currentContentTab == CONTENT_TAB_ALL) {
|
||||||
|
when (state) {
|
||||||
|
is MainContentAllTabUiState.Content -> bindAllTabContent(state)
|
||||||
|
is MainContentAllTabUiState.Empty -> bindAllTabEmpty(state)
|
||||||
|
is MainContentAllTabUiState.Error -> bindAllTabError(state)
|
||||||
|
is MainContentAllTabUiState.Loading -> bindAllTabLoading(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindAllTabContent(state: MainContentAllTabUiState.Content) {
|
||||||
|
bindAllTabControls(state)
|
||||||
|
binding.layoutContentAllSurface.visibility = View.VISIBLE
|
||||||
|
hideAllTabEmptyError()
|
||||||
|
if (state.selectedType.usesSeriesItems()) {
|
||||||
|
binding.rvContentAllItems.adapter = contentAllSeriesCardAdapter
|
||||||
|
contentAllAudioCardAdapter.submitItems(emptyList())
|
||||||
|
contentAllSeriesCardAdapter.submitItems(state.seriesItems)
|
||||||
|
} else {
|
||||||
|
binding.rvContentAllItems.adapter = contentAllAudioCardAdapter
|
||||||
|
contentAllSeriesCardAdapter.submitItems(emptyList())
|
||||||
|
contentAllAudioCardAdapter.submitItems(state.audioItems)
|
||||||
|
}
|
||||||
|
state.paginationErrorMessage?.let { message ->
|
||||||
|
showToast(message)
|
||||||
|
contentAllTabViewModel.consumePaginationErrorMessage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindAllTabEmpty(state: MainContentAllTabUiState.Empty) {
|
||||||
|
bindAllTabControls(state)
|
||||||
|
binding.layoutContentAllSurface.visibility = View.VISIBLE
|
||||||
|
binding.layoutContentAllEmptyError.visibility = View.VISIBLE
|
||||||
|
binding.tvContentAllEmptyError.setText(R.string.screen_content_all_empty)
|
||||||
|
clearAllTabItems()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindAllTabError(state: MainContentAllTabUiState.Error) {
|
||||||
|
bindAllTabControls(state)
|
||||||
|
binding.layoutContentAllSurface.visibility = View.VISIBLE
|
||||||
|
binding.layoutContentAllEmptyError.visibility = View.VISIBLE
|
||||||
|
binding.tvContentAllEmptyError.text = state.message ?: getString(R.string.common_error_unknown)
|
||||||
|
clearAllTabItems()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindAllTabLoading(state: MainContentAllTabUiState.Loading) {
|
||||||
|
bindAllTabControls(state)
|
||||||
|
binding.layoutContentAllSurface.visibility = View.VISIBLE
|
||||||
|
hideAllTabEmptyError()
|
||||||
|
clearAllTabItems()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindAllTabControls(state: MainContentAllTabUiState) {
|
||||||
|
binding.tvContentAllTotalCount.text = state.totalCount.moneyFormat()
|
||||||
|
binding.tvContentAllSortLabel.setText(state.selectedSort.toLabelResId())
|
||||||
|
binding.viewContentAllTypeTabs.root.setMenus(
|
||||||
|
contentAllTypes.map { type -> getString(type.toContentAllTypeLabelResId()) },
|
||||||
|
selectedIndex = contentAllTypes.indexOf(state.selectedType).coerceAtLeast(0)
|
||||||
|
)
|
||||||
|
binding.layoutContentAllDayFilter.visibility = if (state.selectedType.usesDayOfWeekQuery()) View.VISIBLE else View.GONE
|
||||||
|
bindAllDayFilterSelection(state.selectedDayOfWeek)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindAllDayFilterSelection(selectedDayOfWeek: SeriesPublishedDaysOfWeek?) {
|
||||||
|
for (index in 0 until binding.layoutContentAllDayFilter.childCount) {
|
||||||
|
val child = binding.layoutContentAllDayFilter.getChildAt(index) as? TextView ?: continue
|
||||||
|
val isSelected = child.tag == selectedDayOfWeek
|
||||||
|
child.isSelected = isSelected
|
||||||
|
child.setBackgroundResource(if (isSelected) R.drawable.bg_content_all_day_selected else 0)
|
||||||
|
child.setTextColor(
|
||||||
|
ContextCompat.getColor(requireContext(), if (isSelected) R.color.black else R.color.white)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clearAllTabItems() {
|
||||||
|
binding.rvContentAllItems.adapter = null
|
||||||
|
contentAllAudioCardAdapter.submitItems(emptyList())
|
||||||
|
contentAllSeriesCardAdapter.submitItems(emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hideAllTabEmptyError() {
|
||||||
|
binding.layoutContentAllEmptyError.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showAllSortPopup(selectedSort: ContentSort) {
|
||||||
|
sortPopup?.dismiss()
|
||||||
|
sortPopup = CreatorChannelSortPopup(
|
||||||
|
anchor = binding.layoutContentAllSortButton,
|
||||||
|
selectedSort = selectedSort,
|
||||||
|
onSortSelected = { sort ->
|
||||||
|
if (sort in ContentSort.entries) {
|
||||||
|
contentAllTabViewModel.changeSort(sort)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
).also { it.show() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bindContent(content: AudioRecommendationsUiState.Content) {
|
private fun bindContent(content: AudioRecommendationsUiState.Content) {
|
||||||
@@ -299,6 +535,11 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
|||||||
|
|
||||||
private fun openSeriesDetail(item: ContentOriginalSeriesUiModel) {
|
private fun openSeriesDetail(item: ContentOriginalSeriesUiModel) {
|
||||||
val seriesId = item.seriesId.takeIf { it > 0L } ?: return
|
val seriesId = item.seriesId.takeIf { it > 0L } ?: return
|
||||||
|
openSeriesDetail(seriesId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openSeriesDetail(seriesId: Long) {
|
||||||
|
if (seriesId <= 0L) return
|
||||||
startActivity(
|
startActivity(
|
||||||
Intent(requireContext(), SeriesDetailActivity::class.java).apply {
|
Intent(requireContext(), SeriesDetailActivity::class.java).apply {
|
||||||
putExtra(Constants.EXTRA_SERIES_ID, seriesId)
|
putExtra(Constants.EXTRA_SERIES_ID, seriesId)
|
||||||
@@ -312,7 +553,7 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateLoadingDialog() {
|
private fun updateLoadingDialog() {
|
||||||
if (isRecommendationLoading || isRankingLoading) {
|
if (isRecommendationLoading || isRankingLoading || isAllTabLoading) {
|
||||||
loadingDialog.show(screenWidth)
|
loadingDialog.show(screenWidth)
|
||||||
} else {
|
} else {
|
||||||
loadingDialog.dismiss()
|
loadingDialog.dismiss()
|
||||||
@@ -347,5 +588,6 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
|||||||
private const val CONTENT_TAB_RECOMMENDATION = 0
|
private const val CONTENT_TAB_RECOMMENDATION = 0
|
||||||
private const val CONTENT_TAB_RANKING = 1
|
private const val CONTENT_TAB_RANKING = 1
|
||||||
private const val CONTENT_TAB_ALL = 2
|
private const val CONTENT_TAB_ALL = 2
|
||||||
|
private const val CONTENT_ALL_GRID_SPAN_COUNT = 3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,154 @@ class ContentMainFragmentSourceTest {
|
|||||||
assertTrue(source.contains("screen_content_ranking_type_like_count"))
|
assertTrue(source.contains("screen_content_ranking_type_like_count"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `content 전체 layout과 source는 Phase 5 요구를 포함한다`() {
|
||||||
|
val source = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt"
|
||||||
|
).readText()
|
||||||
|
val fragmentLayout = projectFile("app/src/main/res/layout/fragment_v2_main_content.xml").readText()
|
||||||
|
val dayFilterBackground = projectFile("app/src/main/res/drawable/bg_content_all_day_filter.xml").readText()
|
||||||
|
val selectedDayBackground = projectFile("app/src/main/res/drawable/bg_content_all_day_selected.xml").readText()
|
||||||
|
|
||||||
|
assertTrue(fragmentLayout.contains("@+id/layout_content_all_surface"))
|
||||||
|
assertTrue(fragmentLayout.contains("@+id/view_content_all_type_tabs"))
|
||||||
|
assertTrue(fragmentLayout.contains("@+id/layout_content_all_day_filter"))
|
||||||
|
assertTrue(fragmentLayout.contains("android:layout_width=\"wrap_content\""))
|
||||||
|
assertTrue(fragmentLayout.contains("android:layout_height=\"36dp\""))
|
||||||
|
assertTrue(fragmentLayout.contains("android:layout_marginTop=\"@dimen/spacing_8\""))
|
||||||
|
assertTrue(fragmentLayout.contains("android:background=\"@drawable/bg_content_all_day_filter\""))
|
||||||
|
assertTrue(fragmentLayout.contains("android:padding=\"@dimen/spacing_4\""))
|
||||||
|
assertTrue(fragmentLayout.contains("app:layout_constraintEnd_toEndOf=\"parent\""))
|
||||||
|
assertTrue(fragmentLayout.contains("app:layout_constraintHorizontal_bias=\"0.5\""))
|
||||||
|
assertTrue(dayFilterBackground.contains("@color/gray_900"))
|
||||||
|
assertTrue(dayFilterBackground.contains("@dimen/radius_8"))
|
||||||
|
assertTrue(selectedDayBackground.contains("@color/white"))
|
||||||
|
assertTrue(selectedDayBackground.contains("@dimen/radius_8"))
|
||||||
|
assertTrue(fragmentLayout.contains("@+id/layout_content_all_sort_bar"))
|
||||||
|
assertTrue(fragmentLayout.contains("android:layout_height=\"52dp\""))
|
||||||
|
assertTrue(fragmentLayout.contains("@+id/tv_content_all_total_label"))
|
||||||
|
assertTrue(fragmentLayout.contains("android:text=\"@string/screen_content_tab_all\""))
|
||||||
|
assertTrue(fragmentLayout.contains("android:textColor=\"@color/white\""))
|
||||||
|
assertTrue(fragmentLayout.contains("@+id/tv_content_all_total_count"))
|
||||||
|
assertTrue(fragmentLayout.contains("style=\"@style/Typography.Body2\""))
|
||||||
|
assertTrue(fragmentLayout.contains("android:textColor=\"@color/gray_500\""))
|
||||||
|
assertTrue(fragmentLayout.contains("@+id/layout_content_all_sort_button"))
|
||||||
|
assertTrue(fragmentLayout.contains("android:layout_height=\"match_parent\""))
|
||||||
|
assertTrue(fragmentLayout.contains("@+id/tv_content_all_sort_label"))
|
||||||
|
assertTrue(fragmentLayout.contains("style=\"@style/Typography.Body3\""))
|
||||||
|
assertTrue(fragmentLayout.contains("@+id/iv_content_all_sort"))
|
||||||
|
assertTrue(fragmentLayout.contains("android:src=\"@drawable/ic_new_sort\""))
|
||||||
|
assertTrue(fragmentLayout.contains("@+id/rv_content_all_items"))
|
||||||
|
assertTrue(fragmentLayout.contains("@+id/layout_content_all_empty_error"))
|
||||||
|
assertTrue(fragmentLayout.contains("@+id/tv_content_all_empty_error"))
|
||||||
|
|
||||||
|
assertTrue(source.contains("private val contentAllTabViewModel: ContentAllTabViewModel by viewModel()"))
|
||||||
|
assertTrue(source.contains("ContentAllAudioCardAdapter"))
|
||||||
|
assertTrue(source.contains("ContentAllSeriesCardAdapter"))
|
||||||
|
assertTrue(source.contains("CONTENT_ALL_GRID_SPAN_COUNT"))
|
||||||
|
assertTrue(source.contains("GridLayoutManager(requireContext(), CONTENT_ALL_GRID_SPAN_COUNT)"))
|
||||||
|
assertTrue(source.contains("addContentGridItemSpacing(CONTENT_ALL_GRID_SPAN_COUNT)"))
|
||||||
|
assertTrue(source.contains("MainContentAllType.AUDIO"))
|
||||||
|
assertTrue(source.contains("MainContentAllType.SERIES"))
|
||||||
|
assertTrue(source.contains("MainContentAllType.ORIGINAL"))
|
||||||
|
assertTrue(source.contains("MainContentAllType.FREE"))
|
||||||
|
assertTrue(source.contains("MainContentAllType.POINT"))
|
||||||
|
assertFalse(source.contains("screen_content_all_type_all"))
|
||||||
|
assertFalse(source.contains("screen_content_all_type_serialized"))
|
||||||
|
assertTrue(source.contains("CreatorChannelSortPopup"))
|
||||||
|
assertTrue(source.contains("ContentSort.entries"))
|
||||||
|
assertTrue(source.contains("toLabelResId()"))
|
||||||
|
assertTrue(source.contains("contentAllDayOfWeekOptions"))
|
||||||
|
assertTrue(source.contains("toContentAllDayLabelResId()"))
|
||||||
|
assertTrue(source.contains("R.drawable.bg_content_all_day_selected"))
|
||||||
|
assertTrue(source.contains("if (isSelected) R.color.black else R.color.white"))
|
||||||
|
assertTrue(source.contains("loadMore()"))
|
||||||
|
assertTrue(source.contains("consumePaginationErrorMessage()"))
|
||||||
|
assertTrue(source.contains("openAudioContentDetail(audioContentId)"))
|
||||||
|
assertTrue(source.contains("openSeriesDetail(seriesId)"))
|
||||||
|
assertTrue(source.contains("override fun onDestroyView()"))
|
||||||
|
assertTrue(source.contains("sortPopup?.dismiss()"))
|
||||||
|
assertTrue(source.contains("sortPopup = null"))
|
||||||
|
assertTrue(source.contains("bannerBinder = null"))
|
||||||
|
assertTrue(source.contains("layoutContentAllEmptyError"))
|
||||||
|
assertTrue(source.contains("tvContentAllEmptyError.setText"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `content 전체 탭 source는 최신 전체 상태를 보관한다`() {
|
||||||
|
val source = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt"
|
||||||
|
).readText()
|
||||||
|
|
||||||
|
assertSourceContains(
|
||||||
|
source,
|
||||||
|
"private var currentAllTabState: MainContentAllTabUiState? = null",
|
||||||
|
"전체 탭은 Content만 보관하지 않고 Empty Error Loading까지 포함하는 최신 상태를 캐시해야 한다."
|
||||||
|
)
|
||||||
|
assertFalse(
|
||||||
|
"전체 탭 캐시는 MainContentAllTabUiState.Content 전용 currentAllTabContent에 머물면 안 된다.",
|
||||||
|
source.contains("currentAllTabContent: MainContentAllTabUiState.Content?")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `content 전체 탭 observer는 guard 전에 최신 상태를 저장한다`() {
|
||||||
|
val source = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt"
|
||||||
|
).readText()
|
||||||
|
|
||||||
|
assertSourceContains(source, "contentAllTabViewModel.allTabStateLiveData.observe(viewLifecycleOwner) { state ->")
|
||||||
|
assertSourceContains(source, "currentAllTabState = state")
|
||||||
|
assertSourceContains(source, "renderAllTabState(state)")
|
||||||
|
assertTrue(
|
||||||
|
"allTabStateLiveData observer는 render/guard 전에 currentAllTabState = state를 먼저 실행해야 한다.",
|
||||||
|
source.indexOf("currentAllTabState = state") < source.indexOf("renderAllTabState(state)")
|
||||||
|
)
|
||||||
|
assertFalse(
|
||||||
|
"inactive ALL 탭 emission을 버리는 observer/render guard는 제거되어야 한다.",
|
||||||
|
source.contains("if (currentContentTab != CONTENT_TAB_ALL) return")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `content 전체 탭 진입은 캐시 상태를 우선 렌더링하고 필요할 때만 초기 로드한다`() {
|
||||||
|
val source = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt"
|
||||||
|
).readText()
|
||||||
|
|
||||||
|
assertSourceContains(source, "private fun showAllContent()")
|
||||||
|
assertSourceContains(source, "currentAllTabState?.let(::renderAllTabState)")
|
||||||
|
assertSourceContains(source, "if (currentAllTabState == null && !hasSelectedAllTab)")
|
||||||
|
assertSourceContains(source, "contentAllTabViewModel.loadInitial()")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `content 전체 탭 source는 상태별 dispatcher를 둔다`() {
|
||||||
|
val source = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt"
|
||||||
|
).readText()
|
||||||
|
|
||||||
|
assertSourceContains(source, "private fun renderAllTabState(state: MainContentAllTabUiState)")
|
||||||
|
assertSourceContains(source, "is MainContentAllTabUiState.Content ->")
|
||||||
|
assertSourceContains(source, "is MainContentAllTabUiState.Empty ->")
|
||||||
|
assertSourceContains(source, "is MainContentAllTabUiState.Error ->")
|
||||||
|
assertSourceContains(source, "is MainContentAllTabUiState.Loading ->")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `content 전체 탭 source는 비 content 상태에도 공통 control을 바인딩한다`() {
|
||||||
|
val source = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt"
|
||||||
|
).readText()
|
||||||
|
|
||||||
|
assertSourceContains(source, "private fun bindAllTabControls(state: MainContentAllTabUiState)")
|
||||||
|
assertSourceContains(source, "bindAllTabControls(state)")
|
||||||
|
assertSourceContains(source, "binding.viewContentAllTypeTabs.root.setMenus")
|
||||||
|
assertSourceContains(source, "binding.layoutContentAllDayFilter.visibility")
|
||||||
|
assertSourceContains(source, "binding.tvContentAllSortLabel.setText")
|
||||||
|
assertSourceContains(source, "binding.tvContentAllTotalCount.text")
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `content 추천 source는 오디오와 시리즈 routing extra를 사용한다`() {
|
fun `content 추천 source는 오디오와 시리즈 routing extra를 사용한다`() {
|
||||||
val source = projectFile(
|
val source = projectFile(
|
||||||
@@ -207,6 +355,10 @@ class ContentMainFragmentSourceTest {
|
|||||||
link = link
|
link = link
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private fun assertSourceContains(source: String, expected: String, message: String? = null) {
|
||||||
|
assertTrue(message ?: "Expected source to contain: $expected", source.contains(expected))
|
||||||
|
}
|
||||||
|
|
||||||
private fun projectFile(relativePath: String): File {
|
private fun projectFile(relativePath: String): File {
|
||||||
val candidates = listOf(File(relativePath), File("../$relativePath"))
|
val candidates = listOf(File(relativePath), File("../$relativePath"))
|
||||||
return candidates.firstOrNull { it.exists() }
|
return candidates.firstOrNull { it.exists() }
|
||||||
|
|||||||
Reference in New Issue
Block a user