diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/ContentOverviewActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/ContentOverviewActivity.kt new file mode 100644 index 00000000..7f3132d3 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/ContentOverviewActivity.kt @@ -0,0 +1,153 @@ +package kr.co.vividnext.sodalive.v2.main.content.overview + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.View +import androidx.core.view.doOnLayout +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity +import kr.co.vividnext.sodalive.base.BaseActivity +import kr.co.vividnext.sodalive.common.Constants +import kr.co.vividnext.sodalive.databinding.ActivityContentOverviewBinding +import kr.co.vividnext.sodalive.v2.main.content.overview.data.ContentOverviewType +import kr.co.vividnext.sodalive.v2.main.content.overview.model.ContentOverviewUiState +import kr.co.vividnext.sodalive.v2.main.content.overview.model.toTitleResId +import kr.co.vividnext.sodalive.v2.main.content.overview.ui.ContentOverviewAdapter +import kr.co.vividnext.sodalive.v2.main.content.ui.addContentGridItemSpacing +import kr.co.vividnext.sodalive.v2.main.content.ui.calculateContentGridItemWidthPx +import org.koin.androidx.viewmodel.ext.android.viewModel + +class ContentOverviewActivity : BaseActivity( + ActivityContentOverviewBinding::inflate +) { + private val viewModel: ContentOverviewViewModel by viewModel() + private val overviewAdapter = ContentOverviewAdapter(::openAudioContentDetail) + private val overviewType: ContentOverviewType by lazy { intent.contentOverviewType() } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + viewModel.loadFirstPage(overviewType) + } + + override fun setupView() { + setupTitleBar() + setupRecyclerView() + observeViewModel() + } + + private fun setupTitleBar() { + binding.tvContentOverviewTitle.setText(overviewType.toTitleResId()) + binding.ivContentOverviewBack.setOnClickListener { finish() } + } + + private fun setupRecyclerView() { + binding.rvContentOverviewItems.adapter = overviewAdapter + binding.rvContentOverviewItems.layoutManager = GridLayoutManager(this, CONTENT_OVERVIEW_GRID_SPAN_COUNT) + binding.rvContentOverviewItems.addContentGridItemSpacing(CONTENT_OVERVIEW_GRID_SPAN_COUNT) + binding.rvContentOverviewItems.doOnLayout { updateGridItemWidth() } + binding.rvContentOverviewItems.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + if (!recyclerView.canScrollVertically(1)) { + viewModel.loadMore() + } + } + }) + } + + private fun observeViewModel() { + viewModel.isLoading.observe(this) { isLoading -> + binding.pbContentOverviewInitialLoading.visibility = if (isLoading) View.VISIBLE else View.GONE + } + viewModel.overviewStateLiveData.observe(this, ::renderState) + viewModel.toastLiveData.observe(this) { toastMessage -> + toastMessage?.let(::showToast) + } + } + + private fun renderState(state: ContentOverviewUiState) { + when (state) { + is ContentOverviewUiState.Loading -> renderLoading() + is ContentOverviewUiState.Content -> renderContent(state) + is ContentOverviewUiState.Empty -> renderEmpty() + is ContentOverviewUiState.Error -> renderError() + } + } + + private fun renderLoading() { + binding.rvContentOverviewItems.visibility = View.GONE + binding.tvContentOverviewEmptyError.visibility = View.GONE + binding.pbContentOverviewInitialLoading.visibility = View.VISIBLE + binding.pbContentOverviewLoadMore.visibility = View.GONE + } + + private fun renderContent(state: ContentOverviewUiState.Content) { + binding.rvContentOverviewItems.visibility = View.VISIBLE + binding.tvContentOverviewEmptyError.visibility = View.GONE + binding.pbContentOverviewInitialLoading.visibility = View.GONE + binding.pbContentOverviewLoadMore.visibility = if (state.isLoadingMore) View.VISIBLE else View.GONE + overviewAdapter.submitItems(state.items) + updateGridItemWidth() + state.paginationErrorMessage?.let { + showToast(it) + viewModel.consumePaginationErrorMessage() + } + } + + private fun renderEmpty() { + overviewAdapter.submitItems(emptyList()) + binding.rvContentOverviewItems.visibility = View.GONE + binding.tvContentOverviewEmptyError.visibility = View.VISIBLE + binding.pbContentOverviewInitialLoading.visibility = View.GONE + binding.pbContentOverviewLoadMore.visibility = View.GONE + binding.tvContentOverviewEmptyError.setText(R.string.screen_content_all_empty) + } + + private fun renderError() { + overviewAdapter.submitItems(emptyList()) + binding.rvContentOverviewItems.visibility = View.GONE + binding.tvContentOverviewEmptyError.visibility = View.VISIBLE + binding.pbContentOverviewInitialLoading.visibility = View.GONE + binding.pbContentOverviewLoadMore.visibility = View.GONE + binding.tvContentOverviewEmptyError.setText(R.string.common_error_unknown) + } + + private fun updateGridItemWidth() { + val widthPx = binding.rvContentOverviewItems.calculateContentGridItemWidthPx(CONTENT_OVERVIEW_GRID_SPAN_COUNT) + overviewAdapter.setGridItemWidthPx(widthPx) + } + + private fun showToast(toastMessage: kr.co.vividnext.sodalive.common.ToastMessage) { + toastMessage.message?.let { message -> showToast(message) } + ?: toastMessage.resId?.let { resId -> showToast(getString(resId)) } + } + + private fun openAudioContentDetail(contentId: Long) { + if (contentId <= 0) return + + startActivity( + Intent(this, AudioContentDetailActivity::class.java).apply { + putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, contentId) + } + ) + } + + private fun Intent.contentOverviewType(): ContentOverviewType { + val typeName = getStringExtra(EXTRA_CONTENT_OVERVIEW_TYPE) + return ContentOverviewType.entries.firstOrNull { it.name == typeName } + ?: ContentOverviewType.NEW_AND_HOT_AUDIO + } + + companion object { + const val CONTENT_OVERVIEW_GRID_SPAN_COUNT = 2 + private const val EXTRA_CONTENT_OVERVIEW_TYPE = "content_overview_type" + + fun newIntent(context: Context, type: ContentOverviewType): Intent { + return Intent(context, ContentOverviewActivity::class.java) + .putExtra(EXTRA_CONTENT_OVERVIEW_TYPE, type.name) + } + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/ui/ContentOverviewAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/ui/ContentOverviewAdapter.kt new file mode 100644 index 00000000..2c6a79fc --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/ui/ContentOverviewAdapter.kt @@ -0,0 +1,57 @@ +package kr.co.vividnext.sodalive.v2.main.content.overview.ui + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import kr.co.vividnext.sodalive.databinding.ItemContentAudioCardBinding +import kr.co.vividnext.sodalive.extensions.loadUrl +import kr.co.vividnext.sodalive.v2.main.content.overview.model.ContentOverviewUiModel + +class ContentOverviewAdapter( + private val onContentClick: (Long) -> Unit = {} +) : RecyclerView.Adapter() { + + private var items: List = emptyList() + private var gridItemWidthPx: Int = 0 + + fun setGridItemWidthPx(widthPx: Int) { + if (widthPx <= 0 || gridItemWidthPx == widthPx) return + gridItemWidthPx = widthPx + notifyDataSetChanged() + } + + fun submitItems(items: List) { + this.items = items + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return ViewHolder( + ItemContentAudioCardBinding.inflate(LayoutInflater.from(parent.context), parent, false), + onContentClick + ) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(items[position], gridItemWidthPx) + } + + override fun getItemCount(): Int = items.size + + class ViewHolder( + private val binding: ItemContentAudioCardBinding, + private val onContentClick: (Long) -> Unit + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(item: ContentOverviewUiModel, gridItemWidthPx: Int) = with(binding.audioContentCard.root) { + setGridItemWidthPx(gridItemWidthPx) + setContent(item.title, item.creatorNickname) + setTags(item.tags) + setAdultVisible(item.showAdultBadge) + thumbnailView().loadUrl(item.coverImage) + setOnClickListener { + if (item.contentId <= 0) return@setOnClickListener + onContentClick(item.contentId) + } + } + } +} diff --git a/app/src/main/res/layout/activity_content_overview.xml b/app/src/main/res/layout/activity_content_overview.xml new file mode 100644 index 00000000..bdaa2d2d --- /dev/null +++ b/app/src/main/res/layout/activity_content_overview.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + +