재생 목록 수정 페이지 추가

This commit is contained in:
klaus 2024-12-05 18:49:17 +09:00
parent 326ad01983
commit a4b1ef0005
18 changed files with 778 additions and 22 deletions

View File

@ -147,6 +147,7 @@
<activity android:name=".audio_content.series.content.SeriesContentAllActivity" /> <activity android:name=".audio_content.series.content.SeriesContentAllActivity" />
<activity android:name=".audio_content.playlist.detail.AudioContentPlaylistDetailActivity" /> <activity android:name=".audio_content.playlist.detail.AudioContentPlaylistDetailActivity" />
<activity android:name=".audio_content.playlist.create.AudioContentPlaylistCreateActivity" /> <activity android:name=".audio_content.playlist.create.AudioContentPlaylistCreateActivity" />
<activity android:name=".audio_content.playlist.modify.AudioContentPlaylistModifyActivity" />
<activity android:name=".audio_content.box.AudioContentBoxActivity" /> <activity android:name=".audio_content.box.AudioContentBoxActivity" />
<activity android:name=".mypage.alarm.AlarmListActivity" /> <activity android:name=".mypage.alarm.AlarmListActivity" />

View File

@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.audio_content.playlist package kr.co.vividnext.sodalive.audio_content.playlist
import kr.co.vividnext.sodalive.audio_content.playlist.create.CreatePlaylistRequest import kr.co.vividnext.sodalive.audio_content.playlist.create.CreatePlaylistRequest
import kr.co.vividnext.sodalive.audio_content.playlist.modify.UpdatePlaylistRequest
class AudioContentPlaylistRepository(private val api: PlaylistApi) { class AudioContentPlaylistRepository(private val api: PlaylistApi) {
fun getPlaylistList(token: String) = api.getPlaylistList(authHeader = token) fun getPlaylistList(token: String) = api.getPlaylistList(authHeader = token)
@ -25,4 +26,14 @@ class AudioContentPlaylistRepository(private val api: PlaylistApi) {
id = playlistId, id = playlistId,
authHeader = token authHeader = token
) )
fun updatePlaylist(
playlistId: Long,
request: UpdatePlaylistRequest,
token: String
) = api.updatePlaylist(
id = playlistId,
request = request,
authHeader = token
)
} }

View File

@ -3,12 +3,14 @@ package kr.co.vividnext.sodalive.audio_content.playlist
import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.audio_content.playlist.create.CreatePlaylistRequest import kr.co.vividnext.sodalive.audio_content.playlist.create.CreatePlaylistRequest
import kr.co.vividnext.sodalive.audio_content.playlist.detail.GetPlaylistDetailResponse import kr.co.vividnext.sodalive.audio_content.playlist.detail.GetPlaylistDetailResponse
import kr.co.vividnext.sodalive.audio_content.playlist.modify.UpdatePlaylistRequest
import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.ApiResponse
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.DELETE import retrofit2.http.DELETE
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Header import retrofit2.http.Header
import retrofit2.http.POST import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Path import retrofit2.http.Path
interface PlaylistApi { interface PlaylistApi {
@ -34,4 +36,11 @@ interface PlaylistApi {
@Path("id") id: Long, @Path("id") id: Long,
@Header("Authorization") authHeader: String @Header("Authorization") authHeader: String
): Single<ApiResponse<Any>> ): Single<ApiResponse<Any>>
@PUT("/audio-content/playlist/{id}")
fun updatePlaylist(
@Path("id") id: Long,
@Body request: UpdatePlaylistRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
} }

View File

@ -12,6 +12,7 @@ import com.jakewharton.rxbinding4.widget.textChanges
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.audio_content.playlist.create.add_content.PlaylistAddContentDialogFragment import kr.co.vividnext.sodalive.audio_content.playlist.create.add_content.PlaylistAddContentDialogFragment
import kr.co.vividnext.sodalive.audio_content.playlist.detail.AudioContentPlaylistContent
import kr.co.vividnext.sodalive.base.BaseActivity import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.LoadingDialog import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.ActivityAudioContentPlaylistCreateBinding import kr.co.vividnext.sodalive.databinding.ActivityAudioContentPlaylistCreateBinding
@ -29,15 +30,33 @@ class AudioContentPlaylistCreateActivity : BaseActivity<ActivityAudioContentPlay
private lateinit var adapter: AudioContentPlaylistCreateContentAdapter private lateinit var adapter: AudioContentPlaylistCreateContentAdapter
private val addContentDialogFragment: PlaylistAddContentDialogFragment by lazy { private val addContentDialogFragment: PlaylistAddContentDialogFragment by lazy {
PlaylistAddContentDialogFragment(viewModel.contentList) { item, isChecked -> PlaylistAddContentDialogFragment(screenWidth, viewModel.contentList) { item, isChecked ->
when { when {
isChecked -> { isChecked -> {
viewModel.addContentId(item) viewModel.addContentId(
AudioContentPlaylistContent(
id = item.contentId,
title = item.title,
category = item.themeStr,
coverUrl = item.coverImageUrl,
duration = item.duration ?: "00:00:00",
creatorNickname = item.creatorNickname
)
)
return@PlaylistAddContentDialogFragment true return@PlaylistAddContentDialogFragment true
} }
!isChecked -> { !isChecked -> {
viewModel.removeContentId(item) viewModel.removeContentId(
AudioContentPlaylistContent(
id = item.contentId,
title = item.title,
category = item.themeStr,
coverUrl = item.coverImageUrl,
duration = item.duration ?: "00:00:00",
creatorNickname = item.creatorNickname
)
)
return@PlaylistAddContentDialogFragment true return@PlaylistAddContentDialogFragment true
} }

View File

@ -7,26 +7,26 @@ import androidx.recyclerview.widget.RecyclerView
import coil.load import coil.load
import coil.transform.RoundedCornersTransformation import coil.transform.RoundedCornersTransformation
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.order.GetAudioContentOrderListItem import kr.co.vividnext.sodalive.audio_content.playlist.detail.AudioContentPlaylistContent
import kr.co.vividnext.sodalive.databinding.ItemPlaylistCreateContentBinding import kr.co.vividnext.sodalive.databinding.ItemPlaylistCreateContentBinding
import kr.co.vividnext.sodalive.extensions.dpToPx import kr.co.vividnext.sodalive.extensions.dpToPx
class AudioContentPlaylistCreateContentAdapter : class AudioContentPlaylistCreateContentAdapter :
RecyclerView.Adapter<AudioContentPlaylistCreateContentAdapter.ViewHolder>() { RecyclerView.Adapter<AudioContentPlaylistCreateContentAdapter.ViewHolder>() {
private val items = mutableListOf<GetAudioContentOrderListItem>() private val items = mutableListOf<AudioContentPlaylistContent>()
inner class ViewHolder( inner class ViewHolder(
private val binding: ItemPlaylistCreateContentBinding private val binding: ItemPlaylistCreateContentBinding
) : RecyclerView.ViewHolder(binding.root) { ) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetAudioContentOrderListItem) { fun bind(item: AudioContentPlaylistContent) {
binding.ivCover.load(item.coverImageUrl) { binding.ivCover.load(item.coverUrl) {
crossfade(true) crossfade(true)
placeholder(R.drawable.bg_placeholder) placeholder(R.drawable.bg_placeholder)
transformations(RoundedCornersTransformation(5.3f.dpToPx())) transformations(RoundedCornersTransformation(5.3f.dpToPx()))
} }
binding.tvTitle.text = item.title binding.tvTitle.text = item.title
binding.tvTheme.text = item.themeStr binding.tvTheme.text = item.category
binding.tvDuration.text = item.duration binding.tvDuration.text = item.duration
} }
} }
@ -46,7 +46,7 @@ class AudioContentPlaylistCreateContentAdapter :
override fun getItemCount() = items.size override fun getItemCount() = items.size
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
fun updateItems(items: List<GetAudioContentOrderListItem>) { fun updateItems(items: List<AudioContentPlaylistContent>) {
this.items.clear() this.items.clear()
this.items.addAll(items) this.items.addAll(items)
notifyDataSetChanged() notifyDataSetChanged()

View File

@ -6,6 +6,7 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.audio_content.order.GetAudioContentOrderListItem import kr.co.vividnext.sodalive.audio_content.order.GetAudioContentOrderListItem
import kr.co.vividnext.sodalive.audio_content.playlist.AudioContentPlaylistRepository import kr.co.vividnext.sodalive.audio_content.playlist.AudioContentPlaylistRepository
import kr.co.vividnext.sodalive.audio_content.playlist.detail.AudioContentPlaylistContent
import kr.co.vividnext.sodalive.base.BaseViewModel import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.common.SharedPreferenceManager
@ -16,25 +17,25 @@ class AudioContentPlaylistCreateViewModel(
val toastLiveData: LiveData<String?> val toastLiveData: LiveData<String?>
get() = _toastLiveData get() = _toastLiveData
private val _contentListLiveData = MutableLiveData<List<GetAudioContentOrderListItem>>() private val _contentListLiveData = MutableLiveData<List<AudioContentPlaylistContent>>()
val contentListLiveData: LiveData<List<GetAudioContentOrderListItem>> val contentListLiveData: LiveData<List<AudioContentPlaylistContent>>
get() = _contentListLiveData get() = _contentListLiveData
private var _isLoading = MutableLiveData(false) private var _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean> val isLoading: LiveData<Boolean>
get() = _isLoading get() = _isLoading
val contentList = mutableListOf<GetAudioContentOrderListItem>() val contentList = mutableListOf<AudioContentPlaylistContent>()
var title: String = "" var title: String = ""
var desc: String = "" var desc: String = ""
fun addContentId(item: GetAudioContentOrderListItem) { fun addContentId(item: AudioContentPlaylistContent) {
contentList.add(item) contentList.add(item)
_contentListLiveData.value = contentList _contentListLiveData.value = contentList
} }
fun removeContentId(item: GetAudioContentOrderListItem) { fun removeContentId(item: AudioContentPlaylistContent) {
contentList.remove(item) contentList.remove(item)
_contentListLiveData.value = contentList _contentListLiveData.value = contentList
} }
@ -43,7 +44,7 @@ class AudioContentPlaylistCreateViewModel(
if (validate()) { if (validate()) {
_isLoading.value = true _isLoading.value = true
val contentIdAndOrderList = contentList.mapIndexed { index, item -> val contentIdAndOrderList = contentList.mapIndexed { index, item ->
PlaylistContentIdAndOrder(item.contentId, index + 1) PlaylistContentIdAndOrder(item.id, index + 1)
} }
compositeDisposable.add( compositeDisposable.add(

View File

@ -18,14 +18,15 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import kr.co.vividnext.sodalive.audio_content.order.AudioContentOrderListViewModel import kr.co.vividnext.sodalive.audio_content.order.AudioContentOrderListViewModel
import kr.co.vividnext.sodalive.audio_content.order.GetAudioContentOrderListItem import kr.co.vividnext.sodalive.audio_content.order.GetAudioContentOrderListItem
import kr.co.vividnext.sodalive.audio_content.order.OrderType import kr.co.vividnext.sodalive.audio_content.order.OrderType
import kr.co.vividnext.sodalive.audio_content.playlist.create.AudioContentPlaylistCreateActivity import kr.co.vividnext.sodalive.audio_content.playlist.detail.AudioContentPlaylistContent
import kr.co.vividnext.sodalive.common.LoadingDialog import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.FragmentPlaylistAddContentBinding import kr.co.vividnext.sodalive.databinding.FragmentPlaylistAddContentBinding
import kr.co.vividnext.sodalive.extensions.dpToPx import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
class PlaylistAddContentDialogFragment( class PlaylistAddContentDialogFragment(
private val selectedContentIdList: List<GetAudioContentOrderListItem>, private val screenWidth: Int,
private val selectedContentList: List<AudioContentPlaylistContent>,
private val onItemClick: (GetAudioContentOrderListItem, Boolean) -> Boolean private val onItemClick: (GetAudioContentOrderListItem, Boolean) -> Boolean
) : BottomSheetDialogFragment() { ) : BottomSheetDialogFragment() {
@ -86,7 +87,7 @@ class PlaylistAddContentDialogFragment(
loadingDialog = LoadingDialog(requireActivity(), layoutInflater) loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
adapter = PlaylistAddContentAdapter( adapter = PlaylistAddContentAdapter(
selectedContentIdList.map { it.contentId }.toSet(), selectedContentList.map { it.id }.toSet(),
onItemClick onItemClick
) )
@ -157,7 +158,7 @@ class PlaylistAddContentDialogFragment(
viewModel.isLoading.observe(viewLifecycleOwner) { viewModel.isLoading.observe(viewLifecycleOwner) {
if (it) { if (it) {
loadingDialog.show( loadingDialog.show(
(requireActivity() as AudioContentPlaylistCreateActivity).screenWidth, screenWidth,
"" ""
) )
} else { } else {

View File

@ -1,14 +1,17 @@
package kr.co.vividnext.sodalive.audio_content.playlist.detail package kr.co.vividnext.sodalive.audio_content.playlist.detail
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Rect import android.graphics.Rect
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.load import coil.load
import kr.co.vividnext.sodalive.audio_content.playlist.modify.AudioContentPlaylistModifyActivity
import kr.co.vividnext.sodalive.base.BaseActivity import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.base.SodaDialog import kr.co.vividnext.sodalive.base.SodaDialog
import kr.co.vividnext.sodalive.common.Constants import kr.co.vividnext.sodalive.common.Constants
@ -28,6 +31,14 @@ class AudioContentPlaylistDetailActivity : BaseActivity<ActivityAudioContentPlay
private var playlistId: Long = 0 private var playlistId: Long = 0
private val modifyPlaylistResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == RESULT_OK) {
viewModel.getPlaylistDetail(playlistId)
}
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
playlistId = intent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_PLAYLIST_ID, 0) playlistId = intent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_PLAYLIST_ID, 0)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -87,7 +98,19 @@ class AudioContentPlaylistDetailActivity : BaseActivity<ActivityAudioContentPlay
binding.llPlay.setOnClickListener { } binding.llPlay.setOnClickListener { }
binding.llShuffle.setOnClickListener { } binding.llShuffle.setOnClickListener { }
binding.tvBack.setOnClickListener { finish() } binding.tvBack.setOnClickListener { finish() }
binding.ivEdit.setOnClickListener { } binding.ivEdit.setOnClickListener {
modifyPlaylistResult.launch(
Intent(
applicationContext,
AudioContentPlaylistModifyActivity::class.java
).apply {
putExtra(
Constants.EXTRA_AUDIO_CONTENT_PLAYLIST_ID,
playlistId
)
}
)
}
binding.ivMore.setOnClickListener { binding.ivMore.setOnClickListener {
val notifyFragment = AudioContentPlaylistDetailNotifyFragment { val notifyFragment = AudioContentPlaylistDetailNotifyFragment {
SodaDialog( SodaDialog(

View File

@ -0,0 +1,236 @@
package kr.co.vividnext.sodalive.audio_content.playlist.modify
import android.annotation.SuppressLint
import android.app.Service
import android.graphics.Rect
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.View
import android.view.inputmethod.InputMethodManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.jakewharton.rxbinding4.widget.textChanges
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.audio_content.playlist.create.add_content.PlaylistAddContentDialogFragment
import kr.co.vividnext.sodalive.audio_content.playlist.detail.AudioContentPlaylistContent
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.ActivityAudioContentPlaylistModifyBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
class AudioContentPlaylistModifyActivity : BaseActivity<ActivityAudioContentPlaylistModifyBinding>(
ActivityAudioContentPlaylistModifyBinding::inflate
) {
private val viewModel: AudioContentPlaylistModifyViewModel by inject()
private lateinit var imm: InputMethodManager
private lateinit var loadingDialog: LoadingDialog
private lateinit var adapter: AudioContentPlaylistModifyContentAdapter
private var playlistId: Long = 0
private val handler = Handler(Looper.getMainLooper())
private val addContentDialogFragment: PlaylistAddContentDialogFragment by lazy {
PlaylistAddContentDialogFragment(screenWidth, viewModel.contentList) { item, isChecked ->
when {
isChecked -> {
viewModel.addContentId(
AudioContentPlaylistContent(
id = item.contentId,
title = item.title,
category = item.themeStr,
coverUrl = item.coverImageUrl,
duration = item.duration ?: "00:00:00",
creatorNickname = item.creatorNickname
)
)
return@PlaylistAddContentDialogFragment true
}
!isChecked -> {
viewModel.removeContentId(
AudioContentPlaylistContent(
id = item.contentId,
title = item.title,
category = item.themeStr,
coverUrl = item.coverImageUrl,
duration = item.duration ?: "00:00:00",
creatorNickname = item.creatorNickname
)
)
return@PlaylistAddContentDialogFragment true
}
else -> {
return@PlaylistAddContentDialogFragment false
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
playlistId = intent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_PLAYLIST_ID, 0)
super.onCreate(savedInstanceState)
if (playlistId <= 0) {
showToast("잘못된 요청입니다.")
finish()
}
imm = getSystemService(
Service.INPUT_METHOD_SERVICE
) as InputMethodManager
bindData()
viewModel.getPlaylistDetail(playlistId)
}
override fun setupView() {
binding.tvBack.text = "재생목록 수정"
binding.tvBack.setOnClickListener { finish() }
loadingDialog = LoadingDialog(this, layoutInflater)
binding.tvAddContent.setOnClickListener {
hideKeyboard()
if (addContentDialogFragment.isAdded) return@setOnClickListener
addContentDialogFragment.show(supportFragmentManager, addContentDialogFragment.tag)
}
binding.tvModify.setOnClickListener {
hideKeyboard()
viewModel.modifyPlaylist {
setResult(RESULT_OK)
finish()
}
}
adapter = AudioContentPlaylistModifyContentAdapter()
val recyclerView = binding.rvPlaylistContent
recyclerView.setHasFixedSize(true)
recyclerView.layoutManager = LinearLayoutManager(
this,
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)
outRect.left = 13.3f.dpToPx().toInt()
outRect.right = 13.3f.dpToPx().toInt()
when (parent.getChildAdapterPosition(view)) {
0 -> {
outRect.top = 13.3f.dpToPx().toInt()
outRect.bottom = 6.7f.dpToPx().toInt()
}
adapter.itemCount - 1 -> {
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()
}
}
}
})
recyclerView.adapter = adapter
}
private fun bindData() {
viewModel.toastLiveData.observe(this) {
it?.let { showToast(it) }
}
viewModel.isLoading.observe(this) {
if (it) {
loadingDialog.show(screenWidth, "")
} else {
loadingDialog.dismiss()
}
}
viewModel.contentListLiveData.observe(this) {
adapter.updateItems(it)
}
viewModel.detailResponseLiveData.observe(this) {
binding.etTitle.setText(it.title)
binding.etDesc.setText(it.desc)
adapter.updateItems(it.contentList)
}
compositeDisposable.add(
binding.etTitle.textChanges()
.map { it.toString() }
.distinctUntilChanged()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
if (it.length > 30) {
val truncated = it.take(30)
binding.etTitle.setText(truncated)
binding.etTitle.setSelection(truncated.length)
setTitle(truncated)
} else {
setTitle(it)
}
}
)
compositeDisposable.add(
binding.etDesc.textChanges()
.map { it.toString() }
.distinctUntilChanged()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
if (it.length > 40) {
val truncated = it.take(40)
binding.etDesc.setText(truncated)
binding.etDesc.setSelection(truncated.length)
setDesc(truncated)
} else {
setDesc(it)
}
}
)
}
@SuppressLint("SetTextI18n")
private fun setTitle(title: String) {
binding.tvTitleLength.text = "${title.length}/30"
viewModel.title = title
}
@SuppressLint("SetTextI18n")
private fun setDesc(desc: String) {
binding.tvDescLength.text = "${desc.length}/40"
viewModel.desc = desc
}
private fun hideKeyboard() {
handler.postDelayed({
imm.hideSoftInputFromWindow(
window.decorView.applicationWindowToken,
InputMethodManager.HIDE_NOT_ALWAYS
)
}, 100)
}
}

View File

@ -0,0 +1,57 @@
package kr.co.vividnext.sodalive.audio_content.playlist.modify
import android.annotation.SuppressLint
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.audio_content.playlist.detail.AudioContentPlaylistContent
import kr.co.vividnext.sodalive.databinding.ItemPlaylistCreateContentBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
class AudioContentPlaylistModifyContentAdapter :
RecyclerView.Adapter<AudioContentPlaylistModifyContentAdapter.ViewHolder>() {
private val items = mutableListOf<AudioContentPlaylistContent>()
inner class ViewHolder(
private val binding: ItemPlaylistCreateContentBinding
) : 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(
ItemPlaylistCreateContentBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun onBindViewHolder(
holder: ViewHolder,
position: Int
) {
holder.bind(items[position])
}
override fun getItemCount() = items.size
@SuppressLint("NotifyDataSetChanged")
fun updateItems(items: List<AudioContentPlaylistContent>) {
this.items.clear()
this.items.addAll(items)
notifyDataSetChanged()
}
}

View File

@ -0,0 +1,154 @@
package kr.co.vividnext.sodalive.audio_content.playlist.modify
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.audio_content.playlist.create.PlaylistContentIdAndOrder
import kr.co.vividnext.sodalive.audio_content.playlist.detail.AudioContentPlaylistContent
import kr.co.vividnext.sodalive.audio_content.playlist.detail.GetPlaylistDetailResponse
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
class AudioContentPlaylistModifyViewModel(
private val repository: AudioContentPlaylistRepository
) : BaseViewModel() {
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
private val _contentListLiveData = MutableLiveData<List<AudioContentPlaylistContent>>()
val contentListLiveData: LiveData<List<AudioContentPlaylistContent>>
get() = _contentListLiveData
private var _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean>
get() = _isLoading
private val _detailResponseLiveData = MutableLiveData<GetPlaylistDetailResponse>()
val detailResponseLiveData: LiveData<GetPlaylistDetailResponse>
get() = _detailResponseLiveData
val contentList = mutableListOf<AudioContentPlaylistContent>()
var title: String = ""
var desc: String = ""
private var playlistId: Long = 0
fun addContentId(item: AudioContentPlaylistContent) {
contentList.add(item)
_contentListLiveData.value = contentList
}
fun removeContentId(item: AudioContentPlaylistContent) {
contentList.remove(item)
_contentListLiveData.value = contentList
}
fun modifyPlaylist(onSuccess: () -> Unit) {
if (validate()) {
_isLoading.value = true
val contentIdAndOrderList = contentList.mapIndexed { index, item ->
PlaylistContentIdAndOrder(item.id, index + 1)
}
val request = UpdatePlaylistRequest(
title = if (_detailResponseLiveData.value!!.title != title) {
title
} else {
null
},
desc = if (_detailResponseLiveData.value!!.desc != desc) {
desc
} else {
null
},
contentIdAndOrderList = contentIdAndOrderList
)
compositeDisposable.add(
repository.updatePlaylist(
playlistId = playlistId,
request = request,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success) {
onSuccess()
} else {
if (it.message != null) {
_toastLiveData.value = it.message
} else {
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
}
}
},
{
_isLoading.value = false
if (it.message != null) {
_toastLiveData.value = it.message
} else {
_toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
}
}
)
)
}
}
private fun validate(): Boolean {
if (title.isBlank() || title.length < 3) {
_toastLiveData.value = "제목을 3자 이상 입력하세요"
return false
}
if (contentList.isEmpty()) {
_toastLiveData.value = "콘텐츠를 1개 이상 추가하세요"
return false
}
return true
}
fun getPlaylistDetail(playlistId: Long) {
this.playlistId = playlistId
_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) {
val data = it.data
_detailResponseLiveData.value = data!!
this.contentList.addAll(data.contentList)
} 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("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
}

View File

@ -0,0 +1,10 @@
package kr.co.vividnext.sodalive.audio_content.playlist.modify
import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.audio_content.playlist.create.PlaylistContentIdAndOrder
data class UpdatePlaylistRequest(
@SerializedName("title") val title: String? = null,
@SerializedName("desc") val desc: String? = null,
@SerializedName("contentIdAndOrderList") val contentIdAndOrderList: List<PlaylistContentIdAndOrder> = emptyList()
)

View File

@ -29,6 +29,7 @@ import kr.co.vividnext.sodalive.audio_content.playlist.AudioContentPlaylistListV
import kr.co.vividnext.sodalive.audio_content.playlist.PlaylistApi import kr.co.vividnext.sodalive.audio_content.playlist.PlaylistApi
import kr.co.vividnext.sodalive.audio_content.playlist.create.AudioContentPlaylistCreateViewModel import kr.co.vividnext.sodalive.audio_content.playlist.create.AudioContentPlaylistCreateViewModel
import kr.co.vividnext.sodalive.audio_content.playlist.detail.AudioContentPlaylistDetailViewModel import kr.co.vividnext.sodalive.audio_content.playlist.detail.AudioContentPlaylistDetailViewModel
import kr.co.vividnext.sodalive.audio_content.playlist.modify.AudioContentPlaylistModifyViewModel
import kr.co.vividnext.sodalive.audio_content.series.SeriesApi 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.SeriesListAllViewModel
import kr.co.vividnext.sodalive.audio_content.series.SeriesRepository import kr.co.vividnext.sodalive.audio_content.series.SeriesRepository
@ -274,6 +275,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
viewModel { AudioContentPlaylistListViewModel(get()) } viewModel { AudioContentPlaylistListViewModel(get()) }
viewModel { AudioContentPlaylistDetailViewModel(get()) } viewModel { AudioContentPlaylistDetailViewModel(get()) }
viewModel { AudioContentPlaylistCreateViewModel(get()) } viewModel { AudioContentPlaylistCreateViewModel(get()) }
viewModel { AudioContentPlaylistModifyViewModel(get()) }
} }
private val repositoryModule = module { private val repositoryModule = module {

View File

@ -148,7 +148,7 @@
android:drawablePadding="3.5dp" android:drawablePadding="3.5dp"
android:fontFamily="@font/gmarket_sans_bold" android:fontFamily="@font/gmarket_sans_bold"
android:gravity="center_vertical" android:gravity="center_vertical"
android:text="새로운 콘텐츠 추가" android:text="새로운 콘텐츠 추가/제거"
android:textColor="@color/color_3bb9f1" android:textColor="@color/color_3bb9f1"
app:layout_constraintStart_toStartOf="@+id/et_desc" app:layout_constraintStart_toStartOf="@+id/et_desc"
app:layout_constraintTop_toBottomOf="@+id/et_desc" /> app:layout_constraintTop_toBottomOf="@+id/et_desc" />

View File

@ -0,0 +1,167 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black">
<RelativeLayout
android:id="@+id/rl_toolbar"
android:layout_width="0dp"
android:layout_height="51.7dp"
android:background="@color/black"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/tv_back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:drawablePadding="6.7dp"
android:ellipsize="end"
android:fontFamily="@font/gmarket_sans_bold"
android:gravity="center_vertical"
android:minHeight="48dp"
android:paddingHorizontal="13.3dp"
android:text="재생목록 수정"
android:textColor="@color/color_eeeeee"
android:textSize="18.3sp"
app:drawableStartCompat="@drawable/ic_back"
tools:ignore="RelativeOverlap" />
<TextView
android:id="@+id/tv_modify"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:fontFamily="@font/gmarket_sans_medium"
android:gravity="center"
android:minHeight="48dp"
android:paddingHorizontal="13.3dp"
android:text="수정"
android:textColor="@color/color_eeeeee"
android:textSize="14.7sp" />
</RelativeLayout>
<TextView
android:id="@+id/tv_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="13.3dp"
android:layout_marginTop="26.7dp"
android:fontFamily="@font/gmarket_sans_bold"
android:text="재생목록 제목"
android:textColor="@color/color_eeeeee"
android:textSize="16.7sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/rl_toolbar" />
<TextView
android:id="@+id/tv_title_length"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="13.3dp"
android:fontFamily="@font/gmarket_sans_medium"
android:text="0/30"
android:textColor="@color/color_777777"
android:textSize="13.3sp"
app:layout_constraintBottom_toBottomOf="@+id/tv_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/tv_title" />
<EditText
android:id="@+id/et_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="13.3dp"
android:background="@drawable/bg_round_corner_6_7_222222"
android:fontFamily="@font/gmarket_sans_bold"
android:importantForAutofill="no"
android:inputType="textWebEditText"
android:maxLines="1"
android:paddingHorizontal="13.3dp"
android:paddingVertical="17dp"
android:textColor="@color/color_eeeeee"
android:textCursorDrawable="@drawable/edit_text_cursor"
android:textSize="14.7sp"
app:layout_constraintEnd_toEndOf="@+id/tv_title_length"
app:layout_constraintStart_toStartOf="@+id/tv_title"
app:layout_constraintTop_toBottomOf="@+id/tv_title"
tools:text="사랑하는게 어때서?" />
<TextView
android:id="@+id/tv_desc"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="26.7dp"
android:fontFamily="@font/gmarket_sans_bold"
android:text="재생목록 설명을 입력해주세요."
android:textColor="@color/color_eeeeee"
android:textSize="16.7sp"
app:layout_constraintStart_toStartOf="@+id/et_title"
app:layout_constraintTop_toBottomOf="@+id/et_title" />
<TextView
android:id="@+id/tv_desc_length"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fontFamily="@font/gmarket_sans_medium"
android:text="0/40"
android:textColor="@color/color_777777"
android:textSize="13.3sp"
app:layout_constraintBottom_toBottomOf="@+id/tv_desc"
app:layout_constraintEnd_toEndOf="@+id/et_title"
app:layout_constraintTop_toTopOf="@+id/tv_desc" />
<EditText
android:id="@+id/et_desc"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="13.3dp"
android:background="@drawable/bg_round_corner_6_7_222222"
android:fontFamily="@font/gmarket_sans_bold"
android:importantForAutofill="no"
android:inputType="textWebEditText"
android:maxLines="1"
android:minHeight="80dp"
android:paddingHorizontal="13.3dp"
android:paddingVertical="17dp"
android:textColor="@color/color_eeeeee"
android:textCursorDrawable="@drawable/edit_text_cursor"
android:textSize="14.7sp"
app:layout_constraintEnd_toEndOf="@+id/tv_desc_length"
app:layout_constraintStart_toStartOf="@+id/tv_desc"
app:layout_constraintTop_toBottomOf="@+id/tv_desc"
tools:text="사랑하는게 어때서?" />
<TextView
android:id="@+id/tv_add_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="26.7dp"
android:drawableStart="@drawable/btn_plus_round"
android:drawablePadding="3.5dp"
android:fontFamily="@font/gmarket_sans_bold"
android:gravity="center_vertical"
android:text="새로운 콘텐츠 추가/제거"
android:textColor="@color/color_3bb9f1"
app:layout_constraintStart_toStartOf="@+id/et_desc"
app:layout_constraintTop_toBottomOf="@+id/et_desc" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_playlist_content"
android:layout_width="0dp"
android:layout_height="0dp"
android:clipToPadding="false"
android:padding="13.3dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_add_content" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -17,7 +17,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerInParent="true" android:layout_centerInParent="true"
android:fontFamily="@font/gmarket_sans_bold" android:fontFamily="@font/gmarket_sans_bold"
android:text="새로운 콘텐츠 추가" android:text="새로운 콘텐츠 추가/제거"
android:textColor="@color/color_eeeeee" android:textColor="@color/color_eeeeee"
android:textSize="18sp" android:textSize="18sp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"

View File

@ -67,6 +67,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:contentDescription="@null" android:contentDescription="@null"
android:padding="8dp"
android:src="@drawable/ic_playlist_add" android:src="@drawable/ic_playlist_add"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"

View File

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/iv_cover"
android:layout_width="40dp"
android:layout_height="40dp"
android:contentDescription="@null"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_theme"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="13.3dp"
android:background="@drawable/bg_round_corner_2_6_28312b"
android:fontFamily="@font/gmarket_sans_medium"
android:gravity="center"
android:padding="2.6dp"
android:textColor="@color/color_3bac6a"
android:textSize="8sp"
app:layout_constraintStart_toEndOf="@+id/iv_cover"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="SmallSp"
tools:text="커버곡" />
<TextView
android:id="@+id/tv_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:background="@drawable/bg_round_corner_2_6_222222"
android:fontFamily="@font/gmarket_sans_medium"
android:gravity="center"
android:padding="2.6dp"
android:textColor="@color/color_777777"
android:textSize="8sp"
app:layout_constraintStart_toEndOf="@+id/tv_theme"
app:layout_constraintTop_toTopOf="@+id/tv_theme"
tools:ignore="SmallSp"
tools:text="00:30:20" />
<TextView
android:id="@+id/tv_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginVertical="6.7dp"
android:layout_marginEnd="13.3dp"
android:ellipsize="end"
android:fontFamily="@font/gmarket_sans_medium"
android:maxLines="3"
android:textColor="@color/color_d2d2d2"
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/tv_theme"
app:layout_constraintTop_toBottomOf="@+id/tv_theme"
tools:text="안녕하세요 오늘은 커버곡을 들려드리려고 해요안녕하세요 오늘은 커버곡을 들려드리려고 해요안녕하세요 오늘은 커버곡을 들려드리려고 해요안녕하세요 오늘은 커버곡을 들려드리려고 해요" />
</androidx.constraintlayout.widget.ConstraintLayout>