diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/AudioContentPlaylistListAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/AudioContentPlaylistListAdapter.kt index 6dddc63..e9da0fd 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/AudioContentPlaylistListAdapter.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/AudioContentPlaylistListAdapter.kt @@ -55,4 +55,11 @@ class AudioContentPlaylistListAdapter( } override fun getItemCount() = items.count() + + @SuppressLint("NotifyDataSetChanged") + fun updateItems(items: List) { + this.items.clear() + this.items.addAll(items) + notifyDataSetChanged() + } } 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 289a569..b31445c 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 @@ -2,15 +2,20 @@ package kr.co.vividnext.sodalive.audio_content.playlist import android.annotation.SuppressLint import android.content.Intent +import android.graphics.Rect import android.os.Bundle import android.view.View +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity.RESULT_OK import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import kr.co.vividnext.sodalive.audio_content.playlist.create.AudioContentPlaylistCreateActivity 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 kr.co.vividnext.sodalive.extensions.dpToPx import org.koin.android.ext.android.inject class AudioContentPlaylistListFragment : BaseFragment( @@ -22,12 +27,19 @@ class AudioContentPlaylistListFragment : BaseFragment + if (result.resultCode == RESULT_OK) { + viewModel.getPlaylistList() + } + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setupView() bindData() - viewModel.getPlaylistList() } @@ -51,10 +63,47 @@ class AudioContentPlaylistListFragment : BaseFragment { + outRect.top = 0 + outRect.bottom = 5f.dpToPx().toInt() + } + + adapter.itemCount - 1 -> { + outRect.top = 5f.dpToPx().toInt() + outRect.bottom = 0 + } + + else -> { + outRect.top = 5f.dpToPx().toInt() + outRect.bottom = 5f.dpToPx().toInt() + } + } + } + }) + recyclerView.adapter = adapter binding.tvCreatePlaylist.setOnClickListener { - startActivity(Intent(requireContext(), AudioContentPlaylistCreateActivity::class.java)) + createPlaylistResult.launch( + Intent( + requireContext(), + AudioContentPlaylistCreateActivity::class.java + ) + ) } } @@ -86,7 +135,7 @@ class AudioContentPlaylistListFragment : BaseFragment> + + @POST("/audio-content/playlist") + fun createPlaylist( + @Body request: CreatePlaylistRequest, + @Header("Authorization") authHeader: String + ): Single> } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/create/AudioContentPlaylistCreateActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/create/AudioContentPlaylistCreateActivity.kt index 186de3d..58b6e5c 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/create/AudioContentPlaylistCreateActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/create/AudioContentPlaylistCreateActivity.kt @@ -1,22 +1,51 @@ package kr.co.vividnext.sodalive.audio_content.playlist.create +import android.annotation.SuppressLint import android.app.Service +import android.graphics.Rect import android.os.Bundle +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.base.BaseActivity import kr.co.vividnext.sodalive.common.LoadingDialog import kr.co.vividnext.sodalive.databinding.ActivityAudioContentPlaylistCreateBinding +import kr.co.vividnext.sodalive.extensions.dpToPx +import org.koin.android.ext.android.inject class AudioContentPlaylistCreateActivity : BaseActivity( ActivityAudioContentPlaylistCreateBinding::inflate ) { + private val viewModel: AudioContentPlaylistCreateViewModel by inject() + private lateinit var imm: InputMethodManager private lateinit var loadingDialog: LoadingDialog + private lateinit var adapter: AudioContentPlaylistCreateContentAdapter private val addContentDialogFragment: PlaylistAddContentDialogFragment by lazy { - PlaylistAddContentDialogFragment() + PlaylistAddContentDialogFragment(viewModel.contentList) { item, isChecked -> + when { + isChecked -> { + viewModel.addContentId(item) + return@PlaylistAddContentDialogFragment true + } + + !isChecked -> { + viewModel.removeContentId(item) + return@PlaylistAddContentDialogFragment true + } + + else -> { + return@PlaylistAddContentDialogFragment false + } + } + } } override fun onCreate(savedInstanceState: Bundle?) { @@ -25,6 +54,8 @@ class AudioContentPlaylistCreateActivity : BaseActivity { + 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) + } + + 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 } } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/create/AudioContentPlaylistCreateContentAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/create/AudioContentPlaylistCreateContentAdapter.kt new file mode 100644 index 0000000..11a74ea --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/create/AudioContentPlaylistCreateContentAdapter.kt @@ -0,0 +1,54 @@ +package kr.co.vividnext.sodalive.audio_content.playlist.create + +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.order.GetAudioContentOrderListItem +import kr.co.vividnext.sodalive.databinding.ItemPlaylistCreateContentBinding +import kr.co.vividnext.sodalive.extensions.dpToPx + +class AudioContentPlaylistCreateContentAdapter : + RecyclerView.Adapter() { + private val items = mutableListOf() + + inner class ViewHolder( + private val binding: ItemPlaylistCreateContentBinding + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(item: GetAudioContentOrderListItem) { + binding.ivCover.load(item.coverImageUrl) { + crossfade(true) + placeholder(R.drawable.bg_placeholder) + transformations(RoundedCornersTransformation(5.3f.dpToPx())) + } + + binding.tvTitle.text = item.title + binding.tvTheme.text = item.themeStr + 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) { + this.items.clear() + this.items.addAll(items) + notifyDataSetChanged() + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/create/AudioContentPlaylistCreateViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/create/AudioContentPlaylistCreateViewModel.kt new file mode 100644 index 0000000..f4919ac --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/create/AudioContentPlaylistCreateViewModel.kt @@ -0,0 +1,99 @@ +package kr.co.vividnext.sodalive.audio_content.playlist.create + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.schedulers.Schedulers +import kr.co.vividnext.sodalive.audio_content.order.GetAudioContentOrderListItem +import kr.co.vividnext.sodalive.audio_content.playlist.AudioContentPlaylistRepository +import kr.co.vividnext.sodalive.base.BaseViewModel +import kr.co.vividnext.sodalive.common.SharedPreferenceManager + +class AudioContentPlaylistCreateViewModel( + private val repository: AudioContentPlaylistRepository +) : BaseViewModel() { + private val _toastLiveData = MutableLiveData() + val toastLiveData: LiveData + get() = _toastLiveData + + private val _contentListLiveData = MutableLiveData>() + val contentListLiveData: LiveData> + get() = _contentListLiveData + + private var _isLoading = MutableLiveData(false) + val isLoading: LiveData + get() = _isLoading + + val contentList = mutableListOf() + + var title: String = "" + var desc: String = "" + + fun addContentId(item: GetAudioContentOrderListItem) { + contentList.add(item) + _contentListLiveData.value = contentList + } + + fun removeContentId(item: GetAudioContentOrderListItem) { + contentList.remove(item) + _contentListLiveData.value = contentList + } + + fun savePlaylist(onSuccess: () -> Unit) { + if (validate()) { + _isLoading.value = true + val contentIdAndOrderList = contentList.mapIndexed { index, item -> + PlaylistContentIdAndOrder(item.contentId, index + 1) + } + + compositeDisposable.add( + repository.createPlaylist( + request = CreatePlaylistRequest( + title = title, + desc = desc, + contentIdAndOrderList = contentIdAndOrderList + ), + 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 + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/create/CreatePlaylistRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/create/CreatePlaylistRequest.kt new file mode 100644 index 0000000..3428d06 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/create/CreatePlaylistRequest.kt @@ -0,0 +1,12 @@ +package kr.co.vividnext.sodalive.audio_content.playlist.create + +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName + +@Keep +data class CreatePlaylistRequest( + @SerializedName("title") val title: String, + @SerializedName("desc") val desc: String? = null, + @SerializedName("contentIdAndOrderList") + val contentIdAndOrderList: List +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/create/PlaylistContentIdAndOrder.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/create/PlaylistContentIdAndOrder.kt new file mode 100644 index 0000000..5a0ce84 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/create/PlaylistContentIdAndOrder.kt @@ -0,0 +1,10 @@ +package kr.co.vividnext.sodalive.audio_content.playlist.create + +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName + +@Keep +data class PlaylistContentIdAndOrder( + @SerializedName("contentId") val contentId: Long, + @SerializedName("order") val order: Int +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/create/add_content/PlaylistAddContentAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/create/add_content/PlaylistAddContentAdapter.kt index d69f2cd..dd0f726 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/create/add_content/PlaylistAddContentAdapter.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/audio_content/playlist/create/add_content/PlaylistAddContentAdapter.kt @@ -9,11 +9,15 @@ import com.bumptech.glide.Glide import com.bumptech.glide.load.resource.bitmap.CenterCrop import com.bumptech.glide.load.resource.bitmap.RoundedCorners import com.bumptech.glide.request.RequestOptions +import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.audio_content.order.GetAudioContentOrderListItem import kr.co.vividnext.sodalive.databinding.ItemPlaylistAddContentBinding import kr.co.vividnext.sodalive.extensions.dpToPx -class PlaylistAddContentAdapter : RecyclerView.Adapter() { +class PlaylistAddContentAdapter( + private val selectedContentIdList: Set, + private val onItemClick: (GetAudioContentOrderListItem, Boolean) -> Boolean +) : RecyclerView.Adapter() { var items = mutableListOf() @@ -21,7 +25,18 @@ class PlaylistAddContentAdapter : RecyclerView.Adapter, + private val onItemClick: (GetAudioContentOrderListItem, Boolean) -> Boolean +) : BottomSheetDialogFragment() { private lateinit var binding: FragmentPlaylistAddContentBinding private lateinit var adapter: PlaylistAddContentAdapter @@ -70,11 +75,19 @@ class PlaylistAddContentDialogFragment : BottomSheetDialogFragment() { viewModel.getAudioContentOrderList { dismiss() } } + override fun onDismiss(dialog: DialogInterface) { + viewModel.page = 1 + super.onDismiss(dialog) + } + private fun setupView() { binding.tvClose.setOnClickListener { dismiss() } loadingDialog = LoadingDialog(requireActivity(), layoutInflater) - adapter = PlaylistAddContentAdapter() + adapter = PlaylistAddContentAdapter( + selectedContentIdList.map { it.contentId }.toSet(), + onItemClick + ) val recyclerView = binding.rvContent recyclerView.layoutManager = LinearLayoutManager( 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 index 726b672..b0c12e6 100644 --- 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 @@ -1,7 +1,9 @@ package kr.co.vividnext.sodalive.audio_content.playlist.detail +import androidx.annotation.Keep import com.google.gson.annotations.SerializedName +@Keep data class GetPlaylistDetailResponse( @SerializedName("playlistId") val playlistId: Long, @SerializedName("title") val title: String, @@ -12,6 +14,7 @@ data class GetPlaylistDetailResponse( @SerializedName("contentList") val contentList: List ) +@Keep data class AudioContentPlaylistContent( @SerializedName("id") val id: Long, @SerializedName("title") val title: String, 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 f290778..d7a58dc 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 @@ -27,6 +27,7 @@ import kr.co.vividnext.sodalive.audio_content.order.AudioContentOrderListViewMod 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.create.AudioContentPlaylistCreateViewModel 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 @@ -272,6 +273,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) { viewModel { MenuConfigViewModel(get()) } viewModel { AudioContentPlaylistListViewModel(get()) } viewModel { AudioContentPlaylistDetailViewModel(get()) } + viewModel { AudioContentPlaylistCreateViewModel(get()) } } private val repositoryModule = module { diff --git a/app/src/main/res/layout/activity_audio_content_playlist_create.xml b/app/src/main/res/layout/activity_audio_content_playlist_create.xml index a024c63..83d71ce 100644 --- a/app/src/main/res/layout/activity_audio_content_playlist_create.xml +++ b/app/src/main/res/layout/activity_audio_content_playlist_create.xml @@ -44,7 +44,7 @@ android:minHeight="48dp" android:paddingHorizontal="13.3dp" android:text="저장" - android:textColor="@color/color_555555" + android:textColor="@color/color_eeeeee" android:textSize="14.7sp" /> @@ -81,6 +81,9 @@ 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" @@ -122,6 +125,9 @@ 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" diff --git a/app/src/main/res/layout/fragment_playlist_add_content.xml b/app/src/main/res/layout/fragment_playlist_add_content.xml index 3273404..71edb53 100644 --- a/app/src/main/res/layout/fragment_playlist_add_content.xml +++ b/app/src/main/res/layout/fragment_playlist_add_content.xml @@ -1,69 +1,73 @@ - + android:background="@color/black" + android:orientation="vertical"> - + - + - + + + + android:paddingHorizontal="13.3dp"> - + + + + - + android:layout_width="match_parent" + android:layout_height="match_parent" + android:clipToPadding="false" /> + diff --git a/app/src/main/res/layout/item_playlist_create_content.xml b/app/src/main/res/layout/item_playlist_create_content.xml new file mode 100644 index 0000000..fdeaf50 --- /dev/null +++ b/app/src/main/res/layout/item_playlist_create_content.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_playlist_list.xml b/app/src/main/res/layout/item_playlist_list.xml index 6ece239..7d7b65c 100644 --- a/app/src/main/res/layout/item_playlist_list.xml +++ b/app/src/main/res/layout/item_playlist_list.xml @@ -10,6 +10,7 @@ android:layout_width="66.7dp" android:layout_height="66.7dp" android:layout_centerVertical="true" + android:layout_marginTop="8dp" android:contentDescription="@null" />