diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8fd0b098..7bcba105 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -145,6 +145,7 @@ + diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/AudioContentPlaylistListFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/AudioContentPlaylistListFragment.kt index cdde680c..03dd201b 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/AudioContentPlaylistListFragment.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/AudioContentPlaylistListFragment.kt @@ -1,10 +1,13 @@ package kr.co.vividnext.sodalive.audio_content.playlist import android.annotation.SuppressLint +import android.content.Intent import android.os.Bundle import android.view.View import androidx.recyclerview.widget.LinearLayoutManager +import kr.co.vividnext.sodalive.audio_content.playlist.detail.AudioContentPlaylistDetailActivity import kr.co.vividnext.sodalive.base.BaseFragment +import kr.co.vividnext.sodalive.common.Constants import kr.co.vividnext.sodalive.common.LoadingDialog import kr.co.vividnext.sodalive.databinding.FragmentAudioContentPlaylistListBinding import org.koin.android.ext.android.inject @@ -29,7 +32,16 @@ class AudioContentPlaylistListFragment : BaseFragment + startActivity( + Intent(requireContext(), AudioContentPlaylistDetailActivity::class.java).apply { + putExtra( + Constants.EXTRA_AUDIO_CONTENT_PLAYLIST_ID, + playlistId + ) + } + ) + } val recyclerView = binding.rvPlaylistList recyclerView.setHasFixedSize(true) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/AudioContentPlaylistListRepository.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/AudioContentPlaylistListRepository.kt deleted file mode 100644 index 9093868a..00000000 --- a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/AudioContentPlaylistListRepository.kt +++ /dev/null @@ -1,5 +0,0 @@ -package kr.co.vividnext.sodalive.audio_content.playlist - -class AudioContentPlaylistListRepository(private val api: PlaylistApi) { - fun getPlaylistList(token: String) = api.getPlaylistList(authHeader = token) -} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/AudioContentPlaylistListViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/AudioContentPlaylistListViewModel.kt index 47a48c1a..0b088db3 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/AudioContentPlaylistListViewModel.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/AudioContentPlaylistListViewModel.kt @@ -9,7 +9,7 @@ import kr.co.vividnext.sodalive.base.BaseViewModel import kr.co.vividnext.sodalive.common.SharedPreferenceManager class AudioContentPlaylistListViewModel( - private val repository: AudioContentPlaylistListRepository + private val repository: AudioContentPlaylistRepository ) : BaseViewModel() { private val _toastLiveData = MutableLiveData() val toastLiveData: LiveData @@ -28,12 +28,14 @@ class AudioContentPlaylistListViewModel( get() = _playlistLiveData fun getPlaylistList() { + _isLoading.value = true compositeDisposable.add( repository.getPlaylistList(token = "Bearer ${SharedPreferenceManager.token}") .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( { + _isLoading.value = false if (it.success && it.data != null) { _totalCountLiveData.value = it.data.totalCount _playlistLiveData.value = it.data.items @@ -48,6 +50,7 @@ class AudioContentPlaylistListViewModel( } }, { + _isLoading.value = false it.message?.let { message -> Logger.e(message) } _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/AudioContentPlaylistRepository.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/AudioContentPlaylistRepository.kt new file mode 100644 index 00000000..2a2a6d21 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/AudioContentPlaylistRepository.kt @@ -0,0 +1,12 @@ +package kr.co.vividnext.sodalive.audio_content.playlist + +class AudioContentPlaylistRepository(private val api: PlaylistApi) { + fun getPlaylistList(token: String) = api.getPlaylistList(authHeader = token) + fun getPlaylistDetail( + playlistId: Long, + token: String + ) = api.getPlaylistDetail( + id = playlistId, + authHeader = token + ) +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/PlaylistApi.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/PlaylistApi.kt index 94ccc558..e9e3e4ac 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/PlaylistApi.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/PlaylistApi.kt @@ -1,13 +1,21 @@ package kr.co.vividnext.sodalive.audio_content.playlist import io.reactivex.rxjava3.core.Single +import kr.co.vividnext.sodalive.audio_content.playlist.detail.GetPlaylistDetailResponse import kr.co.vividnext.sodalive.common.ApiResponse import retrofit2.http.GET import retrofit2.http.Header +import retrofit2.http.Path interface PlaylistApi { @GET("/audio-content/playlist") fun getPlaylistList( @Header("Authorization") authHeader: String ): Single> + + @GET("/audio-content/playlist/{id}") + fun getPlaylistDetail( + @Path("id") id: Long, + @Header("Authorization") authHeader: String + ): Single> } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/detail/AudioContentPlaylistDetailActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/detail/AudioContentPlaylistDetailActivity.kt new file mode 100644 index 00000000..b6356d74 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/detail/AudioContentPlaylistDetailActivity.kt @@ -0,0 +1,118 @@ +package kr.co.vividnext.sodalive.audio_content.playlist.detail + +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.View +import android.widget.ImageView +import android.widget.Toast +import androidx.recyclerview.widget.LinearLayoutManager +import coil.load +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.ActivityAudioContentPlaylistDetailBinding +import org.koin.android.ext.android.inject + +class AudioContentPlaylistDetailActivity : BaseActivity( + ActivityAudioContentPlaylistDetailBinding::inflate +) { + + private val viewModel: AudioContentPlaylistDetailViewModel by inject() + + private lateinit var loadingDialog: LoadingDialog + private lateinit var adapter: AudioContentPlaylistDetailAdapter + + private var playlistId: Long = 0 + + override fun onCreate(savedInstanceState: Bundle?) { + playlistId = intent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_PLAYLIST_ID, 0) + super.onCreate(savedInstanceState) + + if (playlistId <= 0) { + showToast("잘못된 요청입니다.") + finish() + } + + bindData() + viewModel.getPlaylistDetail(playlistId) + } + + override fun setupView() { + loadingDialog = LoadingDialog(this, layoutInflater) + adapter = AudioContentPlaylistDetailAdapter() + + val recyclerView = binding.rvPlaylistDetail + recyclerView.setHasFixedSize(true) + recyclerView.layoutManager = LinearLayoutManager( + applicationContext, + LinearLayoutManager.VERTICAL, + false + ) + recyclerView.adapter = adapter + + binding.llPlay.setOnClickListener { } + binding.llShuffle.setOnClickListener { } + } + + @SuppressLint("SetTextI18n") + private fun bindData() { + viewModel.toastLiveData.observe(this) { + it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() } + } + + viewModel.isLoading.observe(this) { + if (it) { + loadingDialog.show(screenWidth, "") + } else { + loadingDialog.dismiss() + } + } + + viewModel.detailResponseLiveData.observe(this) { + binding.tvDesc.text = it.desc + binding.tvTitle.text = it.title + binding.tvContentCount.text = " ${it.contentCount}개" + binding.tvCreateDate.text = "만든 날짜 ${it.createdDate} " + updateCoverImageLayout(imageUrlList = it.playlistCoverImageList) + } + } + + private fun updateCoverImageLayout(imageUrlList: List) { + val imageViews = listOf( + binding.ivCover1, + binding.ivCover2, + binding.ivCover3, + binding.ivCover4, + binding.ivCover5, + binding.ivCover6 + ) + + imageViews.forEach { it.visibility = View.GONE } + + when (imageUrlList.size) { + 1, 2 -> { + setImage(imageViews[0], imageUrlList[0]) + } + + 3 -> { + setImage(imageViews[1], imageUrlList[0]) + setImage(imageViews[2], imageUrlList[1]) + setImage(imageViews[3], imageUrlList[2]) + } + + 4 -> { + setImage(imageViews[1], imageUrlList[0]) + setImage(imageViews[2], imageUrlList[1]) + setImage(imageViews[4], imageUrlList[2]) + setImage(imageViews[5], imageUrlList[3]) + } + } + } + + private fun setImage(imageView: ImageView, imageUrl: String) { + imageView.apply { + visibility = View.VISIBLE + this.load(imageUrl) + } + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/detail/AudioContentPlaylistDetailAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/detail/AudioContentPlaylistDetailAdapter.kt new file mode 100644 index 00000000..923f9fe5 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/detail/AudioContentPlaylistDetailAdapter.kt @@ -0,0 +1,46 @@ +package kr.co.vividnext.sodalive.audio_content.playlist.detail + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import coil.load +import coil.transform.RoundedCornersTransformation +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.databinding.ItemPlaylistContentBinding +import kr.co.vividnext.sodalive.extensions.dpToPx + +class AudioContentPlaylistDetailAdapter : + RecyclerView.Adapter() { + + val items = mutableListOf() + + inner class ViewHolder( + private val binding: ItemPlaylistContentBinding + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(item: AudioContentPlaylistContent) { + binding.ivCover.load(item.coverUrl) { + crossfade(true) + placeholder(R.drawable.bg_placeholder) + transformations(RoundedCornersTransformation(5.3f.dpToPx())) + } + + binding.tvTitle.text = item.title + binding.tvTheme.text = item.category + binding.tvDuration.text = item.duration + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( + ItemPlaylistContentBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(items[position]) + } + + override fun getItemCount() = items.count() +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/detail/AudioContentPlaylistDetailViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/detail/AudioContentPlaylistDetailViewModel.kt new file mode 100644 index 00000000..01348269 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/detail/AudioContentPlaylistDetailViewModel.kt @@ -0,0 +1,59 @@ +package kr.co.vividnext.sodalive.audio_content.playlist.detail + +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.playlist.AudioContentPlaylistRepository +import kr.co.vividnext.sodalive.base.BaseViewModel +import kr.co.vividnext.sodalive.common.SharedPreferenceManager + +class AudioContentPlaylistDetailViewModel( + private val repository: AudioContentPlaylistRepository +) : BaseViewModel() { + private val _toastLiveData = MutableLiveData() + val toastLiveData: LiveData + get() = _toastLiveData + + private var _isLoading = MutableLiveData(false) + val isLoading: LiveData + get() = _isLoading + + private val _detailResponseLiveData = MutableLiveData() + val detailResponseLiveData: LiveData + get() = _detailResponseLiveData + + fun getPlaylistDetail(playlistId: Long) { + _isLoading.value = true + compositeDisposable.add( + repository.getPlaylistDetail( + playlistId = playlistId, + token = "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + if (it.success && it.data != null) { + _detailResponseLiveData.value = it.data!! + } 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("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/detail/GetPlaylistDetailResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/detail/GetPlaylistDetailResponse.kt new file mode 100644 index 00000000..726b6727 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/detail/GetPlaylistDetailResponse.kt @@ -0,0 +1,21 @@ +package kr.co.vividnext.sodalive.audio_content.playlist.detail + +import com.google.gson.annotations.SerializedName + +data class GetPlaylistDetailResponse( + @SerializedName("playlistId") val playlistId: Long, + @SerializedName("title") val title: String, + @SerializedName("desc") val desc: String, + @SerializedName("createdDate") val createdDate: String, + @SerializedName("contentCount") val contentCount: Int, + @SerializedName("playlistCoverImageList") val playlistCoverImageList: List, + @SerializedName("contentList") val contentList: List +) + +data class AudioContentPlaylistContent( + @SerializedName("id") val id: Long, + @SerializedName("title") val title: String, + @SerializedName("category") val category: String, + @SerializedName("coverUrl") val coverUrl: String, + @SerializedName("duration") val duration: String +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/common/Constants.kt b/app/src/main/java/kr/co/vividnext/sodalive/common/Constants.kt index 40cff7b5..bff09a34 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/common/Constants.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/common/Constants.kt @@ -41,6 +41,7 @@ object Constants { const val EXTRA_SELECT_RECIPIENT = "extra_select_recipient" const val EXTRA_ROOM_CHANNEL_NAME = "extra_room_channel_name" const val EXTRA_LIVE_RESERVATION_RESPONSE = "extra_live_reservation_response" + const val EXTRA_AUDIO_CONTENT_PLAYLIST_ID = "extra_audio_content_playlist_id" const val EXTRA_AUDIO_CONTENT_ID = "audio_content_id" const val EXTRA_AUDIO_CONTENT_URL = "audio_content_url" 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 6a048106..f2907780 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 @@ -24,9 +24,10 @@ import kr.co.vividnext.sodalive.audio_content.main.ranking.AudioContentMainRanki import kr.co.vividnext.sodalive.audio_content.main.recommend_series.AudioContentMainRecommendSeriesViewModel import kr.co.vividnext.sodalive.audio_content.modify.AudioContentModifyViewModel import kr.co.vividnext.sodalive.audio_content.order.AudioContentOrderListViewModel -import kr.co.vividnext.sodalive.audio_content.playlist.AudioContentPlaylistListRepository +import kr.co.vividnext.sodalive.audio_content.playlist.AudioContentPlaylistRepository import kr.co.vividnext.sodalive.audio_content.playlist.AudioContentPlaylistListViewModel import kr.co.vividnext.sodalive.audio_content.playlist.PlaylistApi +import kr.co.vividnext.sodalive.audio_content.playlist.detail.AudioContentPlaylistDetailViewModel import kr.co.vividnext.sodalive.audio_content.series.SeriesApi import kr.co.vividnext.sodalive.audio_content.series.SeriesListAllViewModel import kr.co.vividnext.sodalive.audio_content.series.SeriesRepository @@ -270,6 +271,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) { viewModel { UserViewModel(get(), get()) } viewModel { MenuConfigViewModel(get()) } viewModel { AudioContentPlaylistListViewModel(get()) } + viewModel { AudioContentPlaylistDetailViewModel(get()) } } private val repositoryModule = module { @@ -298,7 +300,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) { factory { CreatorCommunityRepository(get()) } factory { AlarmListRepository(get()) } factory { MenuConfigRepository(get()) } - factory { AudioContentPlaylistListRepository(get()) } + factory { AudioContentPlaylistRepository(get()) } } private val moduleList = listOf( diff --git a/app/src/main/res/drawable-xxhdpi/ic_playlist_play.png b/app/src/main/res/drawable-xxhdpi/ic_playlist_play.png new file mode 100644 index 00000000..a5c642d3 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_playlist_play.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_playlist_shuffle.png b/app/src/main/res/drawable-xxhdpi/ic_playlist_shuffle.png new file mode 100644 index 00000000..56fa8d45 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_playlist_shuffle.png differ diff --git a/app/src/main/res/layout/activity_audio_content_playlist_detail.xml b/app/src/main/res/layout/activity_audio_content_playlist_detail.xml new file mode 100644 index 00000000..d6b56e4d --- /dev/null +++ b/app/src/main/res/layout/activity_audio_content_playlist_detail.xml @@ -0,0 +1,235 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_playlist_content.xml b/app/src/main/res/layout/item_playlist_content.xml new file mode 100644 index 00000000..6aa01fec --- /dev/null +++ b/app/src/main/res/layout/item_playlist_content.xml @@ -0,0 +1,64 @@ + + + + + + + + + + +