diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 09b08bb..6c28e40 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -116,6 +116,7 @@ + >> + @GET("/audio-content/main/new/all") + fun getNewContentAllOfTheme( + @Query("theme") theme: String, + @Query("page") page: Int, + @Query("size") size: Int, + @Header("Authorization") authHeader: String + ): Single> + @POST("/audio-content/donation") fun donation( @Body request: AudioContentDonationRequest, @@ -154,4 +163,9 @@ interface AudioContentApi { @Query("sort-type") sort: AudioContentViewModel.Sort, @Header("Authorization") authHeader: String ): Single> + + @GET("/audio-content/main/theme") + fun getNewContentThemeList( + @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 bb25cfe..89df5f7 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 @@ -136,6 +136,20 @@ class AudioContentRepository( authHeader = token ) + fun getNewContentAllOfTheme( + theme: String, + page: Int, + size: Int, + token: String + ) = api.getNewContentAllOfTheme( + theme = theme, + page = page - 1, + size = size, + authHeader = token + ) + + fun getNewContentThemeList(token: String) = api.getNewContentThemeList(authHeader = token) + fun donation( contentId: Long, can: Int, diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/all/AudioContentNewAllActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/all/AudioContentNewAllActivity.kt new file mode 100644 index 0000000..4daa4b6 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/all/AudioContentNewAllActivity.kt @@ -0,0 +1,194 @@ +package kr.co.vividnext.sodalive.audio_content.all + +import android.content.Intent +import android.graphics.Rect +import android.os.Bundle +import android.view.View +import android.widget.Toast +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity +import kr.co.vividnext.sodalive.audio_content.main.AudioContentMainNewContentThemeAdapter +import kr.co.vividnext.sodalive.base.BaseActivity +import kr.co.vividnext.sodalive.common.Constants +import kr.co.vividnext.sodalive.common.LoadingDialog +import kr.co.vividnext.sodalive.databinding.ActivityAudioContentNewAllBinding +import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity +import kr.co.vividnext.sodalive.extensions.dpToPx +import org.koin.android.ext.android.inject + +class AudioContentNewAllActivity : BaseActivity( + ActivityAudioContentNewAllBinding::inflate +) { + private val viewModel: AudioContentNewAllViewModel by inject() + + private lateinit var loadingDialog: LoadingDialog + + private lateinit var newContentThemeAdapter: AudioContentMainNewContentThemeAdapter + private lateinit var newContentAdapter: AudioContentNewAllAdapter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + bindData() + viewModel.getThemeList() + viewModel.getNewContentList() + } + + override fun setupView() { + loadingDialog = LoadingDialog(this, layoutInflater) + binding.toolbar.tvBack.text = "새로운 콘텐츠" + binding.toolbar.tvBack.setOnClickListener { finish() } + + setupNewContentTheme() + setupNewContent() + } + + private fun setupNewContentTheme() { + newContentThemeAdapter = AudioContentMainNewContentThemeAdapter { + newContentAdapter.clear() + viewModel.selectTheme(it) + } + + binding.rvNewContentTheme.layoutManager = LinearLayoutManager( + this, + LinearLayoutManager.HORIZONTAL, + false + ) + + binding.rvNewContentTheme.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() + } + + newContentThemeAdapter.itemCount - 1 -> { + outRect.left = 4f.dpToPx().toInt() + outRect.right = 0 + } + + else -> { + outRect.left = 4f.dpToPx().toInt() + outRect.right = 4f.dpToPx().toInt() + } + } + } + }) + + binding.rvNewContentTheme.adapter = newContentThemeAdapter + } + + private fun setupNewContent() { + newContentAdapter = AudioContentNewAllAdapter( + itemWidth = (screenWidth - 40f.dpToPx().toInt()) / 2, + onClickItem = { + startActivity( + Intent(this, AudioContentDetailActivity::class.java).apply { + putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it) + } + ) + }, + onClickCreator = { + startActivity( + Intent(this, UserProfileActivity::class.java).apply { + putExtra(Constants.EXTRA_USER_ID, it) + } + ) + } + ) + + binding.rvContent.layoutManager = GridLayoutManager(this, 2) + + binding.rvContent.addItemDecoration(object : RecyclerView.ItemDecoration() { + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + super.getItemOffsets(outRect, view, parent, state) + + val position = parent.getChildAdapterPosition(view) + if (position % 2 == 0) { + outRect.left = 13.3f.dpToPx().toInt() + outRect.right = 6.7f.dpToPx().toInt() + } else { + outRect.left = 6.7f.dpToPx().toInt() + outRect.right = 13.3f.dpToPx().toInt() + } + + when (position) { + 0, 1 -> { + outRect.top = 13.3f.dpToPx().toInt() + outRect.bottom = 6.7f.dpToPx().toInt() + } + + newContentAdapter.itemCount - 1, newContentAdapter.itemCount - 2 -> { + outRect.top = 6.7f.dpToPx().toInt() + outRect.bottom = 13.3f.dpToPx().toInt() + } + + else -> { + outRect.top = 6.7f.dpToPx().toInt() + outRect.bottom = 6.7f.dpToPx().toInt() + } + } + } + }) + + binding.rvContent.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + + val lastVisibleItemPosition = (recyclerView.layoutManager as LinearLayoutManager?)!! + .findLastCompletelyVisibleItemPosition() + val itemTotalCount = recyclerView.adapter!!.itemCount - 1 + + // 스크롤이 끝에 도달했는지 확인 + if (!recyclerView.canScrollVertically(1) && + lastVisibleItemPosition == itemTotalCount + ) { + viewModel.getNewContentList() + } + } + }) + + binding.rvContent.adapter = newContentAdapter + } + + private fun bindData() { + viewModel.isLoading.observe(this) { + if (it) { + loadingDialog.show(screenWidth) + } else { + loadingDialog.dismiss() + } + } + + viewModel.toastLiveData.observe(this) { + it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() } + } + + viewModel.themeListLiveData.observe(this) { + newContentThemeAdapter.addItems(it) + } + + viewModel.newContentListLiveData.observe(this) { + newContentAdapter.addItems(it) + } + + viewModel.newContentTotalCountLiveData.observe(this) { + binding.tvTotalCount.text = "$it" + } + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/all/AudioContentNewAllAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/all/AudioContentNewAllAdapter.kt new file mode 100644 index 0000000..35d3440 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/all/AudioContentNewAllAdapter.kt @@ -0,0 +1,85 @@ +package kr.co.vividnext.sodalive.audio_content.all + +import android.annotation.SuppressLint +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.recyclerview.widget.RecyclerView +import coil.load +import coil.transform.CircleCropTransformation +import coil.transform.RoundedCornersTransformation +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem +import kr.co.vividnext.sodalive.databinding.ItemAudioContentNewAllBinding +import kr.co.vividnext.sodalive.extensions.dpToPx + +class AudioContentNewAllAdapter( + private val itemWidth: Int, + private val onClickItem: (Long) -> Unit, + private val onClickCreator: (Long) -> Unit, +) : RecyclerView.Adapter() { + + inner class ViewHolder( + private val binding: ItemAudioContentNewAllBinding, + private val onClickItem: (Long) -> Unit, + private val onClickCreator: (Long) -> Unit + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(item: GetAudioContentMainItem) { + binding.ivAudioContentCoverImage.load(item.coverImageUrl) { + crossfade(true) + placeholder(R.drawable.ic_place_holder) + transformations(RoundedCornersTransformation(2.7f.dpToPx())) + + val layoutParams = binding.ivAudioContentCoverImage + .layoutParams as ConstraintLayout.LayoutParams + + layoutParams.width = itemWidth + layoutParams.height = itemWidth + binding.ivAudioContentCoverImage.layoutParams = layoutParams + } + + binding.ivAudioContentCreator.load(item.creatorProfileImageUrl) { + crossfade(true) + placeholder(R.drawable.ic_place_holder) + transformations(CircleCropTransformation()) + } + + binding.tvAudioContentTitle.text = item.title + binding.tvAudioContentCreatorNickname.text = item.creatorNickname + + binding.ivAudioContentCreator.setOnClickListener { onClickCreator(item.creatorId) } + binding.root.setOnClickListener { onClickItem(item.contentId) } + } + } + + private val items = mutableListOf() + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ) = ViewHolder( + ItemAudioContentNewAllBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ), + onClickItem = onClickItem, + onClickCreator = onClickCreator + ) + + override fun getItemCount() = items.size + + override fun onBindViewHolder(holder: AudioContentNewAllAdapter.ViewHolder, position: Int) { + holder.bind(items[position]) + } + + @SuppressLint("NotifyDataSetChanged") + fun addItems(items: List) { + this.items.addAll(items) + notifyDataSetChanged() + } + + fun clear() { + this.items.clear() + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/all/AudioContentNewAllViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/all/AudioContentNewAllViewModel.kt new file mode 100644 index 0000000..f7ff068 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/all/AudioContentNewAllViewModel.kt @@ -0,0 +1,127 @@ +package kr.co.vividnext.sodalive.audio_content.all + +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.GetAudioContentMainItem +import kr.co.vividnext.sodalive.base.BaseViewModel +import kr.co.vividnext.sodalive.common.SharedPreferenceManager + +class AudioContentNewAllViewModel( + private val repository: AudioContentRepository +) : BaseViewModel() { + private val _toastLiveData = MutableLiveData() + val toastLiveData: LiveData + get() = _toastLiveData + + private var _isLoading = MutableLiveData(false) + val isLoading: LiveData + get() = _isLoading + + private var _themeListLiveData = MutableLiveData>() + val themeListLiveData: LiveData> + get() = _themeListLiveData + + private var _newContentListLiveData = MutableLiveData>() + val newContentListLiveData: LiveData> + get() = _newContentListLiveData + + private var _newContentTotalCountLiveData = MutableLiveData() + val newContentTotalCountLiveData: LiveData + get() = _newContentTotalCountLiveData + + private var isLast = false + private var page = 1 + private val size = 10 + private var selectedTheme = "" + + fun getNewContentList() { + if (!_isLoading.value!! && !isLast) { + _isLoading.value = true + + compositeDisposable.add( + repository.getNewContentAllOfTheme( + theme = if (selectedTheme == "전체") { + "" + } else { + selectedTheme + }, + page = page, + size = size, + token = "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + if (it.success && it.data != null) { + if (it.data.items.isNotEmpty()) { + page += 1 + _newContentListLiveData.postValue(it.data.items) + _newContentTotalCountLiveData.postValue(it.data.totalCount) + } else { + isLast = true + } + } 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 getThemeList() { + compositeDisposable.add( + repository.getNewContentThemeList(token = "Bearer ${SharedPreferenceManager.token}") + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + if (it.success && it.data != null) { + val themeList = listOf("전체").union(it.data).toList() + _themeListLiveData.postValue(themeList) + } 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 selectTheme(theme: String) { + isLast = false + page = 1 + selectedTheme = theme + getNewContentList() + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/all/GetNewContentAllResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/all/GetNewContentAllResponse.kt new file mode 100644 index 0000000..7245768 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/all/GetNewContentAllResponse.kt @@ -0,0 +1,9 @@ +package kr.co.vividnext.sodalive.audio_content.all + +import com.google.gson.annotations.SerializedName +import kr.co.vividnext.sodalive.audio_content.main.GetAudioContentMainItem + +data class GetNewContentAllResponse( + @SerializedName("totalCount") val totalCount: Int, + @SerializedName("items") val items: List +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/AudioContentMainFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/AudioContentMainFragment.kt index 38f0f16..342128f 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/AudioContentMainFragment.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/main/AudioContentMainFragment.kt @@ -18,6 +18,7 @@ import com.zhpan.indicator.enums.IndicatorSlideMode import com.zhpan.indicator.enums.IndicatorStyle import kr.co.pointclick.sdk.offerwall.core.PointClickAd import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.audio_content.all.AudioContentNewAllActivity import kr.co.vividnext.sodalive.audio_content.curation.AudioContentCurationActivity import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity import kr.co.vividnext.sodalive.audio_content.order.AudioContentOrderListActivity @@ -138,7 +139,7 @@ class AudioContentMainFragment : BaseFragment( outRect.right = 10.7f.dpToPx().toInt() } - orderListAdapter.itemCount - 1 -> { + newContentCreatorAdapter.itemCount - 1 -> { outRect.left = 10.7f.dpToPx().toInt() outRect.right = 0 } @@ -302,7 +303,7 @@ class AudioContentMainFragment : BaseFragment( outRect.right = 4f.dpToPx().toInt() } - orderListAdapter.itemCount - 1 -> { + newContentThemeAdapter.itemCount - 1 -> { outRect.left = 4f.dpToPx().toInt() outRect.right = 0 } @@ -319,6 +320,10 @@ class AudioContentMainFragment : BaseFragment( } private fun setupNewContent() { + binding.ivNewContentAll.setOnClickListener { + startActivity(Intent(requireContext(), AudioContentNewAllActivity::class.java)) + } + newContentAdapter = AudioContentMainContentAdapter( onClickItem = { startActivity( @@ -357,7 +362,7 @@ class AudioContentMainFragment : BaseFragment( outRect.right = 6.7f.dpToPx().toInt() } - orderListAdapter.itemCount - 1 -> { + newContentAdapter.itemCount - 1 -> { outRect.left = 6.7f.dpToPx().toInt() outRect.right = 0 } 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 6bf9cc7..a9229b5 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 @@ -7,6 +7,7 @@ import kr.co.vividnext.sodalive.audio_content.AudioContentApi import kr.co.vividnext.sodalive.audio_content.AudioContentRepository import kr.co.vividnext.sodalive.audio_content.AudioContentViewModel import kr.co.vividnext.sodalive.audio_content.PlaybackTrackingRepository +import kr.co.vividnext.sodalive.audio_content.all.AudioContentNewAllViewModel import kr.co.vividnext.sodalive.audio_content.comment.AudioContentCommentListViewModel import kr.co.vividnext.sodalive.audio_content.comment.AudioContentCommentReplyViewModel import kr.co.vividnext.sodalive.audio_content.comment.AudioContentCommentRepository @@ -193,6 +194,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) { viewModel { MemberTagViewModel(get()) } viewModel { UserProfileDonationAllViewModel(get()) } viewModel { AudioContentCurationViewModel(get()) } + viewModel { AudioContentNewAllViewModel(get()) } } private val repositoryModule = module { diff --git a/app/src/main/res/layout/activity_audio_content_new_all.xml b/app/src/main/res/layout/activity_audio_content_new_all.xml new file mode 100644 index 0000000..7c5d57f --- /dev/null +++ b/app/src/main/res/layout/activity_audio_content_new_all.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_audio_content_main.xml b/app/src/main/res/layout/fragment_audio_content_main.xml index c648246..cb0168f 100644 --- a/app/src/main/res/layout/fragment_audio_content_main.xml +++ b/app/src/main/res/layout/fragment_audio_content_main.xml @@ -119,14 +119,28 @@ android:orientation="vertical" android:visibility="gone"> - + + + + + + + + + + + + + + + +