diff --git a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/all/player/CreatorCommunityMediaPlayerManager.kt b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/all/player/CreatorCommunityMediaPlayerManager.kt index 92ee620e..32ffe198 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/all/player/CreatorCommunityMediaPlayerManager.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/all/player/CreatorCommunityMediaPlayerManager.kt @@ -1,3 +1,5 @@ +@file:Suppress("ktlint:package-name", "ktlint:standard:package-name") + package kr.co.vividnext.sodalive.explorer.profile.creator_community.all.player import android.content.Context @@ -27,28 +29,36 @@ class CreatorCommunityMediaPlayerManager( private var mediaPlayer: MediaPlayer? = null private var currentPlayingContentId: Long? = null private var isPaused: Boolean = false + private var isPrepared: Boolean = false fun pauseContent() { - mediaPlayer?.pause() + if (isPrepared) { + mediaPlayer?.pause() + } isPaused = true updateUI() } private fun resumeContent() { pauseAudioContentService() - mediaPlayer?.start() + if (isPrepared) { + mediaPlayer?.start() + } isPaused = false updateUI() } fun stopContent() { mediaPlayer?.let { - it.stop() + if (isPrepared) { + it.stop() + } it.release() mediaPlayer = null } currentPlayingContentId = null isPaused = false + isPrepared = false updateUI() } @@ -88,13 +98,15 @@ class CreatorCommunityMediaPlayerManager( try { setDataSource(context, Uri.parse(creatorCommunityContentItem.url)) - prepareAsync() // 비동기적으로 준비 setOnPreparedListener { - start() + isPrepared = true + if (!isPaused) { + start() + } updateUI() // 준비 완료 후 UI 업데이트 } + prepareAsync() // 비동기적으로 준비 } catch (e: IOException) { - e.printStackTrace() Toast.makeText( context, SodaLiveApplicationHolder.get() diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityFragment.kt new file mode 100644 index 00000000..a968215b --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityFragment.kt @@ -0,0 +1,230 @@ +package kr.co.vividnext.sodalive.v2.creator.channel.community + +import android.os.Bundle +import android.view.View +import android.widget.Toast +import androidx.core.view.isVisible +import androidx.core.view.updatePadding +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.LinearLayoutManager +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.base.BaseFragment +import kr.co.vividnext.sodalive.databinding.FragmentCreatorChannelCommunityBinding +import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.player.CreatorCommunityContentItem +import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.player.CreatorCommunityMediaPlayerManager +import kr.co.vividnext.sodalive.extensions.dpToPx +import kr.co.vividnext.sodalive.extensions.moneyFormat +import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityPostUiModel +import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityViewMode +import kr.co.vividnext.sodalive.v2.creator.channel.community.ui.CreatorChannelCommunityGridAdapter +import kr.co.vividnext.sodalive.v2.creator.channel.community.ui.CreatorChannelCommunityListAdapter +import org.koin.androidx.viewmodel.ext.android.viewModel + +class CreatorChannelCommunityFragment : BaseFragment( + FragmentCreatorChannelCommunityBinding::inflate +) { + + private val viewModel: CreatorChannelCommunityViewModel by viewModel() + private val listAdapter = CreatorChannelCommunityListAdapter( + onPlayClick = { item -> toggleCommunityAudio(item) }, + onOwnerMoreClick = { item -> host.onCreatorChannelCommunityOwnerMoreClicked(item) }, + isPlayingContent = { postId -> mediaPlayerManager?.isPlayingContent(postId) == true } + ) + private val gridAdapter = CreatorChannelCommunityGridAdapter() + private var mediaPlayerManager: CreatorCommunityMediaPlayerManager? = null + private var currentContentState: CreatorChannelCommunityUiState.Content? = null + private var lastContentLayoutKey: CreatorChannelCommunityContentLayoutKey? = null + private val creatorId: Long by lazy { arguments?.getLong(ARG_CREATOR_ID) ?: 0L } + private val host: Host + get() = requireActivity() as Host + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + mediaPlayerManager = CreatorCommunityMediaPlayerManager(requireContext()) { listAdapter.notifyDataSetChanged() } + bindLoading() + setupCommunityList() + setupClickListeners() + observeViewModel() + } + + override fun onDestroyView() { + mediaPlayerManager?.stopContent() + mediaPlayerManager = null + currentContentState = null + lastContentLayoutKey = null + binding.rvCreatorChannelCommunity.adapter = null + super.onDestroyView() + } + + override fun onPause() { + mediaPlayerManager?.pauseContent() + super.onPause() + } + + private fun setupCommunityList() = with(binding.rvCreatorChannelCommunity) { + layoutManager = LinearLayoutManager(requireContext()) + adapter = listAdapter + } + + private fun setupClickListeners() = with(binding) { + layoutCreatorChannelCommunityViewModeButton.setOnClickListener { + viewModel.toggleViewMode() + } + btnCreatorChannelCommunityRetry.setOnClickListener { + viewModel.retryCommunity() + } + } + + private fun observeViewModel() { + viewModel.communityStateLiveData.observe(viewLifecycleOwner) { state -> + when (state) { + CreatorChannelCommunityUiState.Loading -> bindLoading() + CreatorChannelCommunityUiState.Empty -> bindEmpty() + is CreatorChannelCommunityUiState.Error -> bindError(state) + is CreatorChannelCommunityUiState.Content -> bindContent(state) + } + } + } + + fun onCreatorChannelCommunityTabSelected() { + if (creatorId > 0L) { + viewModel.loadCommunity(creatorId, isOwner = host.isCreatorChannelOwner()) + } + } + + fun onCreatorChannelCommunityScrolledToBottom() { + viewModel.loadMore() + } + + fun onCreatorChannelCommunityOwnerCtaVisibilityChanged(isVisible: Boolean) { + applyOwnerCtaPadding(isVisible) + } + + private fun bindLoading() = with(binding) { + currentContentState = null + lastContentLayoutKey = null + layoutCreatorChannelCommunitySortBar.isVisible = false + rvCreatorChannelCommunity.isVisible = false + layoutCreatorChannelCommunityEmpty.isVisible = false + tvCreatorChannelCommunityErrorMessage.isVisible = false + btnCreatorChannelCommunityRetry.isVisible = false + } + + private fun bindEmpty() = with(binding) { + currentContentState = null + lastContentLayoutKey = null + layoutCreatorChannelCommunitySortBar.isVisible = false + rvCreatorChannelCommunity.isVisible = false + layoutCreatorChannelCommunityEmpty.isVisible = true + tvCreatorChannelCommunityErrorMessage.isVisible = false + btnCreatorChannelCommunityRetry.isVisible = false + host.onCreatorChannelCommunityContentChanged() + } + + private fun bindError(state: CreatorChannelCommunityUiState.Error) = with(binding) { + currentContentState = null + lastContentLayoutKey = null + layoutCreatorChannelCommunitySortBar.isVisible = false + rvCreatorChannelCommunity.isVisible = false + layoutCreatorChannelCommunityEmpty.isVisible = false + tvCreatorChannelCommunityErrorMessage.isVisible = true + tvCreatorChannelCommunityErrorMessage.text = state.message ?: getString(R.string.creator_channel_community_error_message) + btnCreatorChannelCommunityRetry.isVisible = true + host.onCreatorChannelCommunityContentChanged() + } + + private fun bindContent(state: CreatorChannelCommunityUiState.Content) = with(binding) { + currentContentState = state + layoutCreatorChannelCommunitySortBar.isVisible = true + tvCreatorChannelCommunityTotalCount.text = state.communityPostCount.moneyFormat() + tvCreatorChannelCommunityViewModeLabel.setText(state.viewMode.labelResId) + ivCreatorChannelCommunityViewMode.setImageResource(state.viewMode.iconResId) + rvCreatorChannelCommunity.isVisible = true + bindCommunityAdapter(state) + layoutCreatorChannelCommunityEmpty.isVisible = false + tvCreatorChannelCommunityErrorMessage.isVisible = false + btnCreatorChannelCommunityRetry.isVisible = false + notifyContentChangedIfLayoutChanged(state) + state.paginationErrorMessage?.let { + Toast.makeText(requireContext(), it, Toast.LENGTH_SHORT).show() + viewModel.consumePaginationErrorMessage() + } + } + + private fun bindCommunityAdapter(state: CreatorChannelCommunityUiState.Content) = with(binding.rvCreatorChannelCommunity) { + when (state.viewMode) { + CreatorChannelCommunityViewMode.List -> { + if (adapter !== listAdapter) { + layoutManager = LinearLayoutManager(requireContext()) + adapter = listAdapter + } + listAdapter.submitItems(state.communityPosts) + } + CreatorChannelCommunityViewMode.Grid -> { + if (adapter !== gridAdapter) { + layoutManager = GridLayoutManager(requireContext(), 3) + adapter = gridAdapter + } + gridAdapter.submitItems(state.communityPosts) + } + } + } + + private fun notifyContentChangedIfLayoutChanged(state: CreatorChannelCommunityUiState.Content) { + val contentLayoutKey = state.toContentLayoutKey() + if (contentLayoutKey == lastContentLayoutKey) return + + lastContentLayoutKey = contentLayoutKey + host.onCreatorChannelCommunityContentChanged() + } + + private fun applyOwnerCtaPadding(isVisible: Boolean) = with(binding) { + val bottomPadding = if (isVisible) { + OWNER_CTA_LIST_BOTTOM_PADDING_DP.dpToPx().toInt() + } else { + DEFAULT_LIST_BOTTOM_PADDING_DP.dpToPx().toInt() + } + rvCreatorChannelCommunity.updatePadding(bottom = bottomPadding) + layoutCreatorChannelCommunityEmpty.updatePadding(bottom = bottomPadding) + } + + private fun toggleCommunityAudio(item: CreatorChannelCommunityPostUiModel) { + val audioUrl = item.audioUrl ?: return + mediaPlayerManager?.toggleContent(CreatorCommunityContentItem(item.postId, audioUrl)) + } + + interface Host { + fun isCreatorChannelOwner(): Boolean + fun onCreatorChannelCommunityContentChanged() + fun onCreatorChannelCommunityOwnerMoreClicked(item: CreatorChannelCommunityPostUiModel) { + onCreatorChannelCommunityOwnerMoreClicked(item.postId) + } + fun onCreatorChannelCommunityOwnerMoreClicked(postId: Long) + } + + companion object { + private const val ARG_CREATOR_ID: String = "arg_creator_id" + private const val DEFAULT_LIST_BOTTOM_PADDING_DP = 32 + private const val OWNER_CTA_LIST_BOTTOM_PADDING_DP = 132 + + fun newInstance(creatorId: Long): CreatorChannelCommunityFragment { + return CreatorChannelCommunityFragment().apply { + arguments = Bundle().apply { putLong(ARG_CREATOR_ID, creatorId) } + } + } + } +} + +private data class CreatorChannelCommunityContentLayoutKey( + val communityPostCount: Int, + val viewMode: CreatorChannelCommunityViewMode, + val communityPostIds: List +) + +private fun CreatorChannelCommunityUiState.Content.toContentLayoutKey(): CreatorChannelCommunityContentLayoutKey { + return CreatorChannelCommunityContentLayoutKey( + communityPostCount = communityPostCount, + viewMode = viewMode, + communityPostIds = communityPosts.map { it.postId } + ) +}