diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audition/AuditionApi.kt b/app/src/main/java/kr/co/vividnext/sodalive/audition/AuditionApi.kt new file mode 100644 index 0000000..8ec06a8 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audition/AuditionApi.kt @@ -0,0 +1,16 @@ +package kr.co.vividnext.sodalive.audition + +import io.reactivex.rxjava3.core.Single +import kr.co.vividnext.sodalive.common.ApiResponse +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.Query + +interface AuditionApi { + @GET("/audition") + fun getAuditionList( + @Query("page") page: Int, + @Query("size") size: Int, + @Header("Authorization") authHeader: String + ): Single> +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audition/AuditionFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/audition/AuditionFragment.kt new file mode 100644 index 0000000..57bdbea --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audition/AuditionFragment.kt @@ -0,0 +1,92 @@ +package kr.co.vividnext.sodalive.audition + +import android.graphics.Rect +import android.os.Bundle +import android.view.View +import android.widget.Toast +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import kr.co.vividnext.sodalive.base.BaseFragment +import kr.co.vividnext.sodalive.common.LoadingDialog +import kr.co.vividnext.sodalive.databinding.FragmentAuditionBinding +import kr.co.vividnext.sodalive.extensions.dpToPx +import org.koin.android.ext.android.inject + +class AuditionFragment : BaseFragment( + FragmentAuditionBinding::inflate +) { + private val viewModel: AuditionViewModel by inject() + + private lateinit var adapter: AuditionListAdapter + private lateinit var loadingDialog: LoadingDialog + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupView() + bindData() + + viewModel.getAuditionList() + } + + private fun setupView() { + loadingDialog = LoadingDialog(requireActivity(), layoutInflater) + + val recyclerView = binding.rvAudition + adapter = AuditionListAdapter { } + + recyclerView.layoutManager = LinearLayoutManager( + requireContext(), + LinearLayoutManager.VERTICAL, + false + ) + + recyclerView.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 = 0 + outRect.bottom = 8.3f.dpToPx().toInt() + } + + adapter.itemCount - 1 -> { + outRect.top = 8.3f.dpToPx().toInt() + outRect.bottom = 0 + } + + else -> { + outRect.top = 8.3f.dpToPx().toInt() + outRect.bottom = 8.3f.dpToPx().toInt() + } + } + } + }) + + recyclerView.adapter = adapter + } + + private fun bindData() { + viewModel.toastLiveData.observe(viewLifecycleOwner) { + it?.let { Toast.makeText(requireActivity(), it, Toast.LENGTH_LONG).show() } + } + + viewModel.isLoading.observe(viewLifecycleOwner) { + if (it) { + loadingDialog.show(screenWidth, "") + } else { + loadingDialog.dismiss() + } + } + + viewModel.auditionListLiveData.observe(viewLifecycleOwner) { + adapter.addGroupedList(it) + } + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audition/AuditionListAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/audition/AuditionListAdapter.kt new file mode 100644 index 0000000..317cfed --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audition/AuditionListAdapter.kt @@ -0,0 +1,154 @@ +package kr.co.vividnext.sodalive.audition + +import android.annotation.SuppressLint +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import coil.load +import coil.transform.RoundedCornersTransformation +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.databinding.ItemAuditionListBinding +import kr.co.vividnext.sodalive.databinding.ItemAuditionListCompletedHeaderBinding +import kr.co.vividnext.sodalive.databinding.ItemAuditionListInProgressHeaderBinding +import kr.co.vividnext.sodalive.extensions.dpToPx + +class AuditionListAdapter( + private val onItemClick: (GetAuditionListItem) -> Unit +) : ListAdapter(DiffCallback()) { + companion object { + private const val TYPE_IN_PROGRESS_HEADER = 0 + private const val TYPE_COMPLETED_HEADER = 1 + private const val TYPE_ITEM = 2 + } + + sealed class DisplayItem { + data class InProgressHeader(val count: Int) : DisplayItem() + data class CompletedHeader(val count: Int) : DisplayItem() + data class Data(val item: GetAuditionListItem) : DisplayItem() + } + + class DiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: DisplayItem, newItem: DisplayItem): Boolean { + return oldItem == newItem + } + + override fun areContentsTheSame(oldItem: DisplayItem, newItem: DisplayItem): Boolean { + return oldItem == newItem + } + } + + inner class InProgressHeaderViewHolder( + private val binding: ItemAuditionListInProgressHeaderBinding + ) : RecyclerView.ViewHolder(binding.root) { + @SuppressLint("SetTextI18n") + fun bind(totalCount: Int) { + binding.tvTotalCount.text = "총 ${totalCount}개" + } + } + + inner class CompletedHeaderViewHolder( + private val binding: ItemAuditionListCompletedHeaderBinding + ) : RecyclerView.ViewHolder(binding.root) { + @SuppressLint("SetTextI18n") + fun bind(totalCount: Int) { + binding.tvTotalCount.text = "총 ${totalCount}개" + } + } + + inner class ViewHolder( + private val binding: ItemAuditionListBinding + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(data: DisplayItem.Data) { + binding.tvTitle.text = data.item.title + binding.ivCover.load(data.item.imageUrl) { + crossfade(true) + placeholder(R.drawable.ic_place_holder) + transformations(RoundedCornersTransformation(5f.dpToPx())) + } + + if (data.item.isOff) { + binding.blackCover.visibility = View.VISIBLE + } else { + binding.blackCover.visibility = View.GONE + binding.root.setOnClickListener { onItemClick(data.item) } + } + } + } + + override fun getItemViewType(position: Int): Int { + return when (getItem(position)) { + is DisplayItem.InProgressHeader -> TYPE_IN_PROGRESS_HEADER + is DisplayItem.CompletedHeader -> TYPE_COMPLETED_HEADER + is DisplayItem.Data -> TYPE_ITEM + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return when (viewType) { + TYPE_IN_PROGRESS_HEADER -> { + InProgressHeaderViewHolder( + ItemAuditionListInProgressHeaderBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + TYPE_COMPLETED_HEADER -> { + CompletedHeaderViewHolder( + ItemAuditionListCompletedHeaderBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + TYPE_ITEM -> { + ViewHolder( + ItemAuditionListBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + else -> throw IllegalArgumentException("Invalid view type") + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (val item = getItem(position)) { + is DisplayItem.InProgressHeader -> { + (holder as InProgressHeaderViewHolder).bind(item.count) + } + + is DisplayItem.CompletedHeader -> (holder as CompletedHeaderViewHolder).bind(item.count) + is DisplayItem.Data -> (holder as ViewHolder).bind(item) + } + } + + private val currentItems = mutableListOf() + + fun addGroupedList(newItem: GetAuditionListResponse) { + val isOnItems = newItem.items.filter { !it.isOff } + val isOffItems = newItem.items.filter { it.isOff } + + if (isOnItems.isNotEmpty() && currentItems.none { it is DisplayItem.InProgressHeader }) { + currentItems.add(DisplayItem.InProgressHeader(newItem.inProgressCount)) + } + currentItems.addAll(isOnItems.map { DisplayItem.Data(it) }) + + if (isOffItems.isNotEmpty() && currentItems.none { it is DisplayItem.CompletedHeader }) { + currentItems.add(DisplayItem.CompletedHeader(newItem.completedCount)) + } + currentItems.addAll(isOffItems.map { DisplayItem.Data(it) }) + + submitList(currentItems.toList()) + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audition/AuditionRepository.kt b/app/src/main/java/kr/co/vividnext/sodalive/audition/AuditionRepository.kt new file mode 100644 index 0000000..43cab77 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audition/AuditionRepository.kt @@ -0,0 +1,20 @@ +package kr.co.vividnext.sodalive.audition + +import io.reactivex.rxjava3.core.Single +import kr.co.vividnext.sodalive.common.ApiResponse + +class AuditionRepository( + private val api: AuditionApi +) { + fun getAuditionList( + page: Int, + size: Int, + token: String + ): Single> { + return api.getAuditionList( + page = page - 1, + size = size, + authHeader = token + ) + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audition/AuditionViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/audition/AuditionViewModel.kt new file mode 100644 index 0000000..90e1387 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audition/AuditionViewModel.kt @@ -0,0 +1,68 @@ +package kr.co.vividnext.sodalive.audition + +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.base.BaseViewModel +import kr.co.vividnext.sodalive.common.SharedPreferenceManager + +class AuditionViewModel(private val repository: AuditionRepository) : BaseViewModel() { + private val _toastLiveData = MutableLiveData() + val toastLiveData: LiveData + get() = _toastLiveData + + private var _isLoading = MutableLiveData(false) + val isLoading: LiveData + get() = _isLoading + + private val _auditionListLiveData = MutableLiveData() + val auditionListLiveData: LiveData + get() = _auditionListLiveData + + var page = 1 + var isLast = false + private val pageSize = 10 + + fun getAuditionList() { + if (!isLast && !_isLoading.value!!) { + _isLoading.value = true + compositeDisposable.add( + repository.getAuditionList( + page = page, + size = pageSize, + token = "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + _isLoading.value = false + if (it.success && it.data != null) { + _auditionListLiveData.value = it.data!! + if (it.data.items.isNotEmpty()) { + page += 1 + } else { + isLast = true + } + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + } + }, + { + _isLoading.postValue(false) + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audition/GetAuditionListResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/audition/GetAuditionListResponse.kt new file mode 100644 index 0000000..9d65ab6 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audition/GetAuditionListResponse.kt @@ -0,0 +1,19 @@ +package kr.co.vividnext.sodalive.audition + +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName + +@Keep +data class GetAuditionListResponse( + @SerializedName("inProgressCount") val inProgressCount: Int, + @SerializedName("completedCount") val completedCount: Int, + @SerializedName("items") val items: List +) + +@Keep +data class GetAuditionListItem( + @SerializedName("id") val id: Long, + @SerializedName("title") val title: String, + @SerializedName("imageUrl") val imageUrl: String, + @SerializedName("isOff") val isOff: Boolean +) 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 83bc333..8a33a70 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 @@ -39,6 +39,9 @@ import kr.co.vividnext.sodalive.audio_content.series.content.SeriesContentAllVie import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailViewModel import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadViewModel import kr.co.vividnext.sodalive.audio_content.upload.theme.AudioContentThemeViewModel +import kr.co.vividnext.sodalive.audition.AuditionApi +import kr.co.vividnext.sodalive.audition.AuditionRepository +import kr.co.vividnext.sodalive.audition.AuditionViewModel import kr.co.vividnext.sodalive.common.ApiBuilder import kr.co.vividnext.sodalive.common.ObjectBox import kr.co.vividnext.sodalive.explorer.ExplorerApi @@ -200,6 +203,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) { single { ApiBuilder().build(get(), CreatorCommunityApi::class.java) } single { ApiBuilder().build(get(), CategoryApi::class.java) } single { ApiBuilder().build(get(), PlaylistApi::class.java) } + single { ApiBuilder().build(get(), AuditionApi::class.java) } } private val viewModelModule = module { @@ -279,6 +283,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) { viewModel { AudioContentPlaylistCreateViewModel(get()) } viewModel { AudioContentPlaylistModifyViewModel(get()) } viewModel { AudioContentPlayerViewModel() } + viewModel { AuditionViewModel(get()) } } private val repositoryModule = module { @@ -309,6 +314,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) { factory { MenuConfigRepository(get()) } factory { AudioContentPlaylistRepository(get()) } factory { AudioContentGenerateUrlRepository(get()) } + factory { AuditionRepository(get()) } } private val moduleList = listOf( diff --git a/app/src/main/java/kr/co/vividnext/sodalive/main/MainActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/main/MainActivity.kt index cb5e4d6..c5bb859 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/main/MainActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/main/MainActivity.kt @@ -33,13 +33,13 @@ import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity import kr.co.vividnext.sodalive.audio_content.main.AudioContentMainFragment import kr.co.vividnext.sodalive.audio_content.player.AudioContentPlayerFragment import kr.co.vividnext.sodalive.audio_content.player.AudioContentPlayerService +import kr.co.vividnext.sodalive.audition.AuditionFragment 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.common.SharedPreferenceManager import kr.co.vividnext.sodalive.databinding.ActivityMainBinding import kr.co.vividnext.sodalive.databinding.ItemMainTabBinding -import kr.co.vividnext.sodalive.explorer.ExplorerFragment import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity import kr.co.vividnext.sodalive.extensions.dpToPx import kr.co.vividnext.sodalive.live.LiveFragment @@ -254,7 +254,7 @@ class MainActivity : BaseActivity(ActivityMainBinding::infl if (roomId > 0) { changeFragment(MainViewModel.CurrentTab.LIVE) setTabSelected(binding.tabLive, isSelected = true) - setTabSelected(binding.tabExplorer, isSelected = false) + setTabSelected(binding.tabAudition, isSelected = false) setTabSelected(binding.tabMessage, isSelected = false) setTabSelected(binding.tabMy, isSelected = false) setTabSelected(binding.tabContent, isSelected = false) @@ -281,7 +281,7 @@ class MainActivity : BaseActivity(ActivityMainBinding::infl handler.postDelayed({ changeFragment(MainViewModel.CurrentTab.MESSAGE) setTabSelected(binding.tabLive, isSelected = false) - setTabSelected(binding.tabExplorer, isSelected = false) + setTabSelected(binding.tabAudition, isSelected = false) setTabSelected(binding.tabMessage, isSelected = true) setTabSelected(binding.tabMy, isSelected = false) setTabSelected(binding.tabContent, isSelected = false) @@ -316,14 +316,14 @@ class MainActivity : BaseActivity(ActivityMainBinding::infl ) setupTab( - binding = binding.tabExplorer, - title = "탐색", - imageSrc = R.drawable.ic_tabbar_explorer, + binding = binding.tabAudition, + title = "오디션", + imageSrc = R.drawable.ic_tabbar_audition, colorStateList = ContextCompat.getColorStateList( applicationContext, R.color.color_tabbar_title ), - tab = MainViewModel.CurrentTab.EXPLORER + tab = MainViewModel.CurrentTab.AUDITION ) setupTab( @@ -351,7 +351,7 @@ class MainActivity : BaseActivity(ActivityMainBinding::infl viewModel.currentTab.observe(this) { setTabSelected(binding.tabContent, isSelected = false) setTabSelected(binding.tabLive, isSelected = false) - setTabSelected(binding.tabExplorer, isSelected = false) + setTabSelected(binding.tabAudition, isSelected = false) setTabSelected(binding.tabMessage, isSelected = false) setTabSelected(binding.tabMy, isSelected = false) @@ -365,8 +365,8 @@ class MainActivity : BaseActivity(ActivityMainBinding::infl setTabSelected(binding.tabLive, isSelected = true) } - MainViewModel.CurrentTab.EXPLORER -> { - setTabSelected(binding.tabExplorer, isSelected = true) + MainViewModel.CurrentTab.AUDITION -> { + setTabSelected(binding.tabAudition, isSelected = true) } MainViewModel.CurrentTab.MESSAGE -> { @@ -424,7 +424,7 @@ class MainActivity : BaseActivity(ActivityMainBinding::infl fragment = when (currentTab) { MainViewModel.CurrentTab.LIVE -> liveFragment MainViewModel.CurrentTab.CONTENT -> AudioContentMainFragment() - MainViewModel.CurrentTab.EXPLORER -> ExplorerFragment() + MainViewModel.CurrentTab.AUDITION -> AuditionFragment() MainViewModel.CurrentTab.MESSAGE -> MessageFragment() MainViewModel.CurrentTab.MY -> MyPageFragment() } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/main/MainViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/main/MainViewModel.kt index 9fbafb1..32c9a13 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/main/MainViewModel.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/main/MainViewModel.kt @@ -32,8 +32,8 @@ class MainViewModel( @SerializedName("LIVE") LIVE, - @SerializedName("EXPLORER") - EXPLORER, + @SerializedName("AUDITION") + AUDITION, @SerializedName("MESSAGE") MESSAGE, diff --git a/app/src/main/res/drawable-xxhdpi/ic_tabbar_audition_normal.png b/app/src/main/res/drawable-xxhdpi/ic_tabbar_audition_normal.png new file mode 100644 index 0000000..be44b2a Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_tabbar_audition_normal.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_tabbar_audition_selected.png b/app/src/main/res/drawable-xxhdpi/ic_tabbar_audition_selected.png new file mode 100644 index 0000000..0c15f1c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_tabbar_audition_selected.png differ diff --git a/app/src/main/res/drawable/ic_tabbar_explorer.xml b/app/src/main/res/drawable/ic_tabbar_audition.xml similarity index 60% rename from app/src/main/res/drawable/ic_tabbar_explorer.xml rename to app/src/main/res/drawable/ic_tabbar_audition.xml index f1493fb..185cbc8 100644 --- a/app/src/main/res/drawable/ic_tabbar_explorer.xml +++ b/app/src/main/res/drawable/ic_tabbar_audition.xml @@ -1,5 +1,5 @@ - - + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index b2f5dda..b2ae649 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -177,7 +177,7 @@ android:layout_weight="1" /> + + + + + + diff --git a/app/src/main/res/layout/item_audition_list.xml b/app/src/main/res/layout/item_audition_list.xml new file mode 100644 index 0000000..9b50e4a --- /dev/null +++ b/app/src/main/res/layout/item_audition_list.xml @@ -0,0 +1,45 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/item_audition_list_completed_header.xml b/app/src/main/res/layout/item_audition_list_completed_header.xml new file mode 100644 index 0000000..99b7494 --- /dev/null +++ b/app/src/main/res/layout/item_audition_list_completed_header.xml @@ -0,0 +1,41 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/item_audition_list_in_progress_header.xml b/app/src/main/res/layout/item_audition_list_in_progress_header.xml new file mode 100644 index 0000000..84331ce --- /dev/null +++ b/app/src/main/res/layout/item_audition_list_in_progress_header.xml @@ -0,0 +1,41 @@ + + + + + + + + +