feat(original): UI 변경
- 캐릭터 / 작품 정보 탭 추가
- 작품 정보 탭 구성
- 작품 소개
- 원작 보러 가기
- 상세 정보
- 작가
- 제작사
- 원작
This commit is contained in:
@@ -1,12 +1,15 @@
|
||||
package kr.co.vividnext.sodalive.chat.character
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
@Keep
|
||||
data class Character(
|
||||
@SerializedName("characterId") val characterId: Long,
|
||||
@SerializedName("name") val name: String,
|
||||
@SerializedName("description") val description: String,
|
||||
@SerializedName("imageUrl") val imageUrl: String
|
||||
)
|
||||
) : Parcelable
|
||||
|
||||
@@ -20,12 +20,4 @@ interface OriginalWorkApi {
|
||||
@Header("Authorization") authHeader: String,
|
||||
@Path("id") id: Long
|
||||
): Single<ApiResponse<OriginalWorkDetailResponse>>
|
||||
|
||||
@GET("/api/chat/original/{id}/characters")
|
||||
fun getOriginalWorkCharacters(
|
||||
@Header("Authorization") authHeader: String,
|
||||
@Path("id") id: Long,
|
||||
@Query("page") page: Int,
|
||||
@Query("size") size: Int
|
||||
): Single<ApiResponse<OriginalWorkCharactersPageResponse>>
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package kr.co.vividnext.sodalive.chat.original
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kr.co.vividnext.sodalive.chat.character.Character
|
||||
|
||||
@Parcelize
|
||||
@Keep
|
||||
data class OriginalWorkDetailResponse(
|
||||
@SerializedName("imageUrl") val imageUrl: String?,
|
||||
@@ -12,6 +15,11 @@ data class OriginalWorkDetailResponse(
|
||||
@SerializedName("category") val category: String,
|
||||
@SerializedName("isAdult") val isAdult: Boolean,
|
||||
@SerializedName("description") val description: String,
|
||||
@SerializedName("originalWork") val originalWork: String?,
|
||||
@SerializedName("originalLink") val originalLink: String?,
|
||||
@SerializedName("writer") val writer: String?,
|
||||
@SerializedName("studio") val studio: String?,
|
||||
@SerializedName("originalLinks") val originalLinks: List<String>,
|
||||
@SerializedName("tags") val tags: List<String>,
|
||||
@SerializedName("characters") val characters: List<Character>
|
||||
)
|
||||
) : Parcelable
|
||||
|
||||
@@ -20,13 +20,4 @@ class OriginalWorkRepository(
|
||||
): Single<ApiResponse<OriginalWorkDetailResponse>> {
|
||||
return api.getOriginalWorkDetail(token, id)
|
||||
}
|
||||
|
||||
fun getOriginalCharacters(
|
||||
token: String,
|
||||
id: Long,
|
||||
page: Int,
|
||||
size: Int
|
||||
): Single<ApiResponse<OriginalWorkCharactersPageResponse>> {
|
||||
return api.getOriginalWorkCharacters(token, id, page, size)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package kr.co.vividnext.sodalive.chat.original.detail
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import kr.co.vividnext.sodalive.base.BaseFragment
|
||||
import kr.co.vividnext.sodalive.chat.character.detail.CharacterDetailActivity
|
||||
import kr.co.vividnext.sodalive.chat.character.detail.CharacterDetailActivity.Companion.EXTRA_CHARACTER_ID
|
||||
import kr.co.vividnext.sodalive.chat.original.OriginalWorkDetailResponse
|
||||
import kr.co.vividnext.sodalive.common.GridSpacingItemDecoration
|
||||
import kr.co.vividnext.sodalive.databinding.FragmentOriginalWorkCharacterBinding
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
|
||||
class OriginalWorkCharacterFragment : BaseFragment<FragmentOriginalWorkCharacterBinding>(
|
||||
FragmentOriginalWorkCharacterBinding::inflate
|
||||
) {
|
||||
private var originalWorkDetailResponse: OriginalWorkDetailResponse? = null
|
||||
|
||||
private lateinit var adapter: OriginalWorkDetailAdapter
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (arguments != null) {
|
||||
originalWorkDetailResponse =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
requireArguments().getParcelable(
|
||||
OriginalWorkDetailActivity.EXTRA_ORIGINAL_WORK_DETAIL,
|
||||
OriginalWorkDetailResponse::class.java
|
||||
)
|
||||
} else {
|
||||
requireArguments().getParcelable(
|
||||
OriginalWorkDetailActivity.EXTRA_ORIGINAL_WORK_DETAIL
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
if (originalWorkDetailResponse != null) {
|
||||
setupRecycler()
|
||||
adapter.setItems(originalWorkDetailResponse!!.characters)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupRecycler() {
|
||||
adapter = OriginalWorkDetailAdapter(
|
||||
onClickCharacter = { characterId ->
|
||||
startActivity(
|
||||
Intent(
|
||||
requireContext(),
|
||||
CharacterDetailActivity::class.java
|
||||
).apply {
|
||||
putExtra(EXTRA_CHARACTER_ID, characterId)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
val spanCount = 2
|
||||
val spacingPx = 16f.dpToPx().toInt()
|
||||
binding.rvCharacter.layoutManager = GridLayoutManager(requireContext(), spanCount)
|
||||
binding.rvCharacter.addItemDecoration(
|
||||
GridSpacingItemDecoration(
|
||||
spanCount,
|
||||
spacingPx,
|
||||
true
|
||||
)
|
||||
)
|
||||
|
||||
binding.rvCharacter.adapter = adapter
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,16 @@
|
||||
package kr.co.vividnext.sodalive.chat.original.detail
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.core.net.toUri
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import coil.load
|
||||
import coil.size.Scale
|
||||
import coil.transform.BlurTransformation
|
||||
import coil.transform.RoundedCornersTransformation
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||
import kr.co.vividnext.sodalive.chat.character.detail.CharacterDetailActivity
|
||||
import kr.co.vividnext.sodalive.chat.character.detail.CharacterDetailActivity.Companion.EXTRA_CHARACTER_ID
|
||||
import kr.co.vividnext.sodalive.common.GridSpacingItemDecoration
|
||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||
import kr.co.vividnext.sodalive.databinding.ActivityOriginalWorkDetailBinding
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import org.koin.android.ext.android.inject
|
||||
@@ -23,25 +21,29 @@ class OriginalWorkDetailActivity : BaseActivity<ActivityOriginalWorkDetailBindin
|
||||
|
||||
companion object {
|
||||
const val EXTRA_ORIGINAL_ID = "extra_original_id"
|
||||
const val EXTRA_ORIGINAL_WORK_DETAIL = "extra_original_work_detail"
|
||||
}
|
||||
|
||||
private val viewModel: OriginalWorkDetailViewModel by inject()
|
||||
|
||||
private lateinit var adapter: OriginalWorkDetailAdapter
|
||||
|
||||
private var originalId: Long = -1
|
||||
private lateinit var loadingDialog: LoadingDialog
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
originalId = intent.getLongExtra(EXTRA_ORIGINAL_ID, -1)
|
||||
|
||||
setupRecycler()
|
||||
val originalId = intent.getLongExtra(EXTRA_ORIGINAL_ID, -1)
|
||||
if (originalId <= 0) {
|
||||
Toast.makeText(applicationContext, "잘못된 요청입니다.", Toast.LENGTH_LONG).show()
|
||||
finish()
|
||||
}
|
||||
bind()
|
||||
|
||||
if (originalId > 0) viewModel.loadDetail(originalId)
|
||||
viewModel.loadDetail(originalId)
|
||||
}
|
||||
|
||||
override fun setupView() {
|
||||
loadingDialog = LoadingDialog(this, layoutInflater)
|
||||
|
||||
// 배경 이미지 높이를 화면 너비 비율에 맞게 설정(306:432)
|
||||
binding.ivBg.post {
|
||||
val width = binding.ivBg.width.takeIf { it > 0 } ?: resources.displayMetrics.widthPixels
|
||||
@@ -53,65 +55,106 @@ class OriginalWorkDetailActivity : BaseActivity<ActivityOriginalWorkDetailBindin
|
||||
|
||||
// Toolbar back
|
||||
binding.ivBack.setOnClickListener { finish() }
|
||||
|
||||
setupTabs()
|
||||
}
|
||||
|
||||
private fun setupRecycler() {
|
||||
adapter = OriginalWorkDetailAdapter(
|
||||
onClickOpenLink = { url ->
|
||||
startActivity(Intent(Intent.ACTION_VIEW, url.toUri()))
|
||||
},
|
||||
onClickCharacter = { characterId ->
|
||||
startActivity(
|
||||
Intent(this, CharacterDetailActivity::class.java).apply {
|
||||
putExtra(EXTRA_CHARACTER_ID, characterId)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
val spanCount = 2
|
||||
val spacingPx = 16f.dpToPx().toInt()
|
||||
val layoutManager = GridLayoutManager(this, spanCount)
|
||||
layoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
||||
override fun getSpanSize(position: Int): Int {
|
||||
return if (adapter.getItemViewType(position) == 0) spanCount else 1
|
||||
}
|
||||
}
|
||||
binding.rvDetail.layoutManager = layoutManager
|
||||
binding.rvDetail.addItemDecoration(GridSpacingItemDecoration(spanCount, spacingPx, true, headerCount = 1))
|
||||
binding.rvDetail.adapter = adapter
|
||||
private fun setupTabs() {
|
||||
val tabs = binding.tabs
|
||||
tabs.addTab(tabs.newTab().setText("캐릭터").setTag("character"))
|
||||
tabs.addTab(tabs.newTab().setText("작품정보").setTag("info"))
|
||||
|
||||
binding.rvDetail.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
if (!recyclerView.canScrollVertically(1)) {
|
||||
if (originalId > 0) viewModel.loadMoreCharacters(originalId)
|
||||
}
|
||||
tabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
||||
override fun onTabSelected(tab: TabLayout.Tab) {
|
||||
val tag = tab.tag as String
|
||||
changeFragment(tag)
|
||||
}
|
||||
|
||||
override fun onTabUnselected(tab: TabLayout.Tab) {
|
||||
}
|
||||
|
||||
override fun onTabReselected(tab: TabLayout.Tab) {
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
private fun changeFragment(tag: String) {
|
||||
val fragmentManager = supportFragmentManager
|
||||
val fragmentTransaction = fragmentManager.beginTransaction()
|
||||
|
||||
val fragment = when (tag) {
|
||||
"info" -> OriginalWorkInfoFragment()
|
||||
else -> OriginalWorkCharacterFragment()
|
||||
}
|
||||
|
||||
val bundle = Bundle()
|
||||
bundle.putParcelable(EXTRA_ORIGINAL_WORK_DETAIL, viewModel.detailResponse)
|
||||
fragment.arguments = bundle
|
||||
|
||||
fragmentTransaction.replace(R.id.container, fragment, tag)
|
||||
fragmentTransaction.setPrimaryNavigationFragment(fragment)
|
||||
fragmentTransaction.setReorderingAllowed(true)
|
||||
fragmentTransaction.commitNow()
|
||||
}
|
||||
|
||||
private fun bind() {
|
||||
viewModel.detail.observe(this) { data ->
|
||||
adapter.setHeader(data)
|
||||
// 배경 이미지 Blur 처리 및 채우기
|
||||
val imageUrl = data?.imageUrl
|
||||
if (!imageUrl.isNullOrBlank()) {
|
||||
binding.ivBg.load(imageUrl) {
|
||||
transformations(
|
||||
BlurTransformation(
|
||||
this@OriginalWorkDetailActivity,
|
||||
25f,
|
||||
2.5f
|
||||
)
|
||||
)
|
||||
scale(Scale.FILL)
|
||||
}
|
||||
viewModel.toast.observe(this) {
|
||||
it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() }
|
||||
}
|
||||
|
||||
viewModel.isLoading.observe(this) {
|
||||
if (it) {
|
||||
loadingDialog.show(screenWidth, "")
|
||||
} else {
|
||||
binding.ivBg.setImageResource(R.drawable.bg_placeholder)
|
||||
loadingDialog.dismiss()
|
||||
}
|
||||
}
|
||||
viewModel.characters.observe(this) { list ->
|
||||
adapter.setItems(list)
|
||||
|
||||
viewModel.detail.observe(this) { data ->
|
||||
if (data != null) {
|
||||
// 배경 이미지 Blur 처리 및 채우기
|
||||
val imageUrl = data.imageUrl
|
||||
if (!imageUrl.isNullOrBlank()) {
|
||||
binding.ivBg.load(imageUrl) {
|
||||
transformations(
|
||||
BlurTransformation(
|
||||
this@OriginalWorkDetailActivity,
|
||||
25f,
|
||||
2.5f
|
||||
)
|
||||
)
|
||||
scale(Scale.FILL)
|
||||
}
|
||||
} else {
|
||||
binding.ivBg.setImageResource(R.drawable.bg_placeholder)
|
||||
}
|
||||
|
||||
binding.ivCover.load(data.imageUrl) {
|
||||
crossfade(true)
|
||||
placeholder(R.drawable.bg_placeholder)
|
||||
transformations(RoundedCornersTransformation(16f.dpToPx()))
|
||||
}
|
||||
|
||||
binding.tvTitle.text = data.title
|
||||
binding.tvContentType.text = data.contentType
|
||||
binding.tvCategory.text = data.category
|
||||
binding.tvTags.text = data.tags.joinToString(" ") {
|
||||
if (it.startsWith("#")) {
|
||||
it
|
||||
} else {
|
||||
"#$it"
|
||||
}
|
||||
}
|
||||
|
||||
binding.tvAdult.visibility = if (data.isAdult) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
|
||||
changeFragment("character")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,128 +1,24 @@
|
||||
package kr.co.vividnext.sodalive.chat.original.detail
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.text.TextUtils
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.load
|
||||
import coil.transform.RoundedCornersTransformation
|
||||
import com.orhanobut.logger.Logger
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.chat.character.Character
|
||||
import kr.co.vividnext.sodalive.chat.original.OriginalWorkDetailResponse
|
||||
import kr.co.vividnext.sodalive.databinding.ItemOriginalDetailCharacterBinding
|
||||
import kr.co.vividnext.sodalive.databinding.ItemOriginalDetailHeaderBinding
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
|
||||
class OriginalWorkDetailAdapter(
|
||||
private val onClickOpenLink: (String) -> Unit,
|
||||
private var items: List<Character> = emptyList(),
|
||||
private val onClickCharacter: (Long) -> Unit
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
// 작품소개 확장 상태 (헤더 1개이므로 어댑터 레벨에서 유지)
|
||||
private var isDescriptionExpanded: Boolean = false
|
||||
|
||||
companion object {
|
||||
private const val TYPE_HEADER = 0
|
||||
private const val TYPE_ITEM = 1
|
||||
}
|
||||
|
||||
private var header: OriginalWorkDetailResponse? = null
|
||||
private val items = mutableListOf<Character>()
|
||||
|
||||
fun setHeader(data: OriginalWorkDetailResponse?) {
|
||||
header = data
|
||||
notifyItemChanged(0)
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun setItems(chars: List<Character>) {
|
||||
items.clear()
|
||||
items.addAll(chars)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int = if (position == 0) TYPE_HEADER else TYPE_ITEM
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return if (viewType == TYPE_HEADER) {
|
||||
val binding = ItemOriginalDetailHeaderBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
HeaderVH(binding)
|
||||
} else {
|
||||
val binding = ItemOriginalDetailCharacterBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
ItemVH(binding)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = 1 + items.size
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
if (holder is HeaderVH) {
|
||||
holder.bind(header)
|
||||
} else if (holder is ItemVH) {
|
||||
holder.bind(items[position - 1])
|
||||
}
|
||||
}
|
||||
|
||||
inner class HeaderVH(private val binding: ItemOriginalDetailHeaderBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
private fun applyDescriptionState() {
|
||||
if (isDescriptionExpanded) {
|
||||
binding.tvDescription.maxLines = Int.MAX_VALUE
|
||||
binding.tvDescription.ellipsize = null
|
||||
} else {
|
||||
binding.tvDescription.maxLines = 2
|
||||
binding.tvDescription.ellipsize = TextUtils.TruncateAt.END
|
||||
}
|
||||
}
|
||||
|
||||
fun bind(data: OriginalWorkDetailResponse?) {
|
||||
if (data == null) return
|
||||
|
||||
// Cover small card
|
||||
binding.ivCover.load(data.imageUrl) {
|
||||
crossfade(true)
|
||||
placeholder(R.drawable.bg_placeholder)
|
||||
transformations(RoundedCornersTransformation(16f.dpToPx()))
|
||||
}
|
||||
|
||||
binding.tvTitle.text = data.title
|
||||
binding.tvContentType.text = data.contentType
|
||||
binding.tvCategory.text = data.category
|
||||
binding.tvDescription.text = data.description
|
||||
|
||||
binding.tvAdult.visibility = if (data.isAdult) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
|
||||
// 설명 토글 (2줄/전체)
|
||||
applyDescriptionState()
|
||||
binding.tvDescription.setOnClickListener {
|
||||
isDescriptionExpanded = !isDescriptionExpanded
|
||||
applyDescriptionState()
|
||||
}
|
||||
|
||||
binding.tvOpenOriginal.isEnabled = !data.originalLink.isNullOrBlank()
|
||||
binding.tvOpenOriginal.alpha = if (data.originalLink.isNullOrBlank()) 0.5f else 1f
|
||||
binding.tvOpenOriginal.setOnClickListener {
|
||||
data.originalLink?.let { onClickOpenLink(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class ItemVH(private val binding: ItemOriginalDetailCharacterBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
) : RecyclerView.Adapter<OriginalWorkDetailAdapter.ItemVH>() {
|
||||
inner class ItemVH(
|
||||
private val binding: ItemOriginalDetailCharacterBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(item: Character) {
|
||||
binding.tvCharacterName.text = item.name
|
||||
binding.tvCharacterDescription.text = item.description
|
||||
@@ -134,4 +30,25 @@ class OriginalWorkDetailAdapter(
|
||||
binding.root.setOnClickListener { onClickCharacter(item.characterId) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemVH(
|
||||
ItemOriginalDetailCharacterBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
|
||||
override fun onBindViewHolder(holder: ItemVH, position: Int) {
|
||||
holder.bind(items[position])
|
||||
Logger.d("onBindViewHolder: $position")
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = items.size
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun setItems(chars: List<Character>) {
|
||||
items = chars
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,12 +23,7 @@ class OriginalWorkDetailViewModel(
|
||||
private val _detail = MutableLiveData<OriginalWorkDetailResponse?>(null)
|
||||
val detail: LiveData<OriginalWorkDetailResponse?> get() = _detail
|
||||
|
||||
private val _characters = MutableLiveData<List<Character>>(emptyList())
|
||||
val characters: LiveData<List<Character>> get() = _characters
|
||||
|
||||
private val size = 20
|
||||
private var page = 1 // 초기 로딩 이후부터 사용하므로 1부터 시작
|
||||
private var isLast = false
|
||||
lateinit var detailResponse: OriginalWorkDetailResponse
|
||||
|
||||
fun loadDetail(id: Long) {
|
||||
if (_isLoading.value == true) return
|
||||
@@ -43,46 +38,8 @@ class OriginalWorkDetailViewModel(
|
||||
.subscribe({ response ->
|
||||
val data = response.data
|
||||
if (response.success && data != null) {
|
||||
_detail.value = data
|
||||
// 상세 응답 내 캐릭터를 초기 세팅
|
||||
_characters.value = data.characters
|
||||
// 초기 캐릭터가 없으면 다음 로딩에서 page=1 그대로 시도
|
||||
page = 1
|
||||
isLast = false
|
||||
} else {
|
||||
_toast.value = response.message ?: "알 수 없는 오류가 발생했습니다."
|
||||
}
|
||||
_isLoading.value = false
|
||||
}, { e ->
|
||||
_isLoading.value = false
|
||||
_toast.value = e.message ?: "알 수 없는 오류가 발생했습니다."
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
fun loadMoreCharacters(id: Long) {
|
||||
if (_isLoading.value == true || isLast) return
|
||||
_isLoading.value = true
|
||||
compositeDisposable.add(
|
||||
repository.getOriginalCharacters(
|
||||
token = "Bearer ${SharedPreferenceManager.token}",
|
||||
id = id,
|
||||
page = page,
|
||||
size = size
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ response ->
|
||||
val data = response.data
|
||||
if (response.success && data != null) {
|
||||
val current = _characters.value ?: emptyList()
|
||||
val next = current + data.content
|
||||
_characters.value = next
|
||||
if (data.content.isNotEmpty()) {
|
||||
page += 1
|
||||
} else {
|
||||
isLast = true
|
||||
}
|
||||
detailResponse = data
|
||||
_detail.value = detailResponse
|
||||
} else {
|
||||
_toast.value = response.message ?: "알 수 없는 오류가 발생했습니다."
|
||||
}
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
package kr.co.vividnext.sodalive.chat.original.detail
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import kr.co.vividnext.sodalive.base.BaseFragment
|
||||
import kr.co.vividnext.sodalive.chat.original.OriginalWorkDetailResponse
|
||||
import kr.co.vividnext.sodalive.databinding.FragmentOriginalWorkInfoBinding
|
||||
|
||||
class OriginalWorkInfoFragment : BaseFragment<FragmentOriginalWorkInfoBinding>(
|
||||
FragmentOriginalWorkInfoBinding::inflate
|
||||
) {
|
||||
private var originalWorkDetailResponse: OriginalWorkDetailResponse? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (arguments != null) {
|
||||
originalWorkDetailResponse =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
requireArguments().getParcelable(
|
||||
OriginalWorkDetailActivity.EXTRA_ORIGINAL_WORK_DETAIL,
|
||||
OriginalWorkDetailResponse::class.java
|
||||
)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
requireArguments().getParcelable(
|
||||
OriginalWorkDetailActivity.EXTRA_ORIGINAL_WORK_DETAIL
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val data = originalWorkDetailResponse ?: return
|
||||
|
||||
// 1. 작품 소개
|
||||
binding.tvDesc.text = data.description
|
||||
|
||||
// 2-3. 원작 보러 가기 섹션
|
||||
val links = data.originalLinks
|
||||
if (links.isEmpty()) {
|
||||
binding.llOriginalLink.isGone = true
|
||||
} else {
|
||||
binding.llOriginalLink.isVisible = true
|
||||
binding.llOriginalLinks.removeAllViews()
|
||||
links.forEachIndexed { index, url ->
|
||||
val tv = createLinkTextView(url, index)
|
||||
binding.llOriginalLinks.addView(tv)
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 상세 정보 - 작가
|
||||
val writer = data.writer
|
||||
if (writer.isNullOrBlank()) {
|
||||
binding.tvLabelWriter.isGone = true
|
||||
binding.tvWriter.isGone = true
|
||||
} else {
|
||||
binding.tvLabelWriter.isVisible = true
|
||||
binding.tvWriter.isVisible = true
|
||||
binding.tvWriter.text = writer
|
||||
}
|
||||
|
||||
// 4. 상세 정보 - 제작사
|
||||
val studio = data.studio
|
||||
if (studio.isNullOrBlank()) {
|
||||
binding.tvLabelStudio.isGone = true
|
||||
binding.tvStudio.isGone = true
|
||||
} else {
|
||||
binding.tvLabelStudio.isVisible = true
|
||||
binding.tvStudio.isVisible = true
|
||||
binding.tvStudio.text = studio
|
||||
}
|
||||
|
||||
// 4. 상세 정보 - 원작 (원작명 + 링크)
|
||||
val originalWork = data.originalWork
|
||||
val originalLink = data.originalLink
|
||||
if (originalWork.isNullOrBlank()) {
|
||||
binding.tvLabelOriginal.isGone = true
|
||||
binding.tvOriginalWork.isGone = true
|
||||
} else {
|
||||
binding.tvLabelOriginal.isVisible = true
|
||||
binding.tvOriginalWork.isVisible = true
|
||||
binding.tvOriginalWork.text = originalWork
|
||||
if (!originalLink.isNullOrBlank()) {
|
||||
binding.tvOriginalWork.isClickable = true
|
||||
// 밑줄 표시로 링크 가능함을 시각적으로 안내
|
||||
binding.tvOriginalWork.paintFlags =
|
||||
binding.tvOriginalWork.paintFlags or android.graphics.Paint.UNDERLINE_TEXT_FLAG
|
||||
// Ripple 효과 추가로 터치 피드백 제공
|
||||
runCatching {
|
||||
val outValue = android.util.TypedValue()
|
||||
requireContext().theme.resolveAttribute(
|
||||
android.R.attr.selectableItemBackground,
|
||||
outValue,
|
||||
true
|
||||
)
|
||||
binding.tvOriginalWork.setBackgroundResource(outValue.resourceId)
|
||||
}
|
||||
// 접근성 설명
|
||||
binding.tvOriginalWork.contentDescription = "원작 $originalWork 링크 열기"
|
||||
|
||||
binding.tvOriginalWork.setOnClickListener {
|
||||
openUrl(originalLink)
|
||||
}
|
||||
} else {
|
||||
binding.tvOriginalWork.isClickable = false
|
||||
// 링크가 없을 경우 밑줄/리플 제거
|
||||
binding.tvOriginalWork.paintFlags =
|
||||
binding.tvOriginalWork.paintFlags and android.graphics.Paint.UNDERLINE_TEXT_FLAG.inv()
|
||||
binding.tvOriginalWork.setBackgroundResource(0)
|
||||
binding.tvOriginalWork.contentDescription = originalWork
|
||||
binding.tvOriginalWork.setOnClickListener(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createLinkTextView(url: String, index: Int): TextView {
|
||||
val tv = TextView(requireContext())
|
||||
tv.text = extractDisplayText(url, index)
|
||||
tv.setTextColor(requireContext().getColor(android.R.color.white))
|
||||
tv.textSize = 14f
|
||||
tv.isClickable = true
|
||||
tv.setOnClickListener { openUrl(url) }
|
||||
|
||||
val lp = ViewGroup.MarginLayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
lp.rightMargin = (8 * resources.displayMetrics.density).toInt()
|
||||
lp.topMargin = (4 * resources.displayMetrics.density).toInt()
|
||||
tv.layoutParams = lp
|
||||
|
||||
tv.setPadding(
|
||||
(12 * resources.displayMetrics.density).toInt(),
|
||||
(6 * resources.displayMetrics.density).toInt(),
|
||||
(12 * resources.displayMetrics.density).toInt(),
|
||||
(6 * resources.displayMetrics.density).toInt()
|
||||
)
|
||||
// Chip 같은 느낌의 배경이 프로젝트에 없을 수 있어 기본 투명 배경 유지
|
||||
return tv
|
||||
}
|
||||
|
||||
private fun extractDisplayText(url: String, index: Int): String {
|
||||
return try {
|
||||
val uri = url.toUri()
|
||||
val host = uri.host
|
||||
if (!host.isNullOrBlank()) host else url
|
||||
} catch (_: Exception) {
|
||||
// 파싱 실패 시 간단한 레이블 제공
|
||||
"링크 ${index + 1}"
|
||||
}
|
||||
}
|
||||
|
||||
private fun openUrl(url: String) {
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
|
||||
startActivity(intent)
|
||||
} catch (_: Exception) {
|
||||
// 안전상 silently ignore 또는 토스트 노출이 가능 하다면 추가
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,14 +47,137 @@
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rv_detail"
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:clipToPadding="false"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/rl_toolbar"
|
||||
tools:listitem="@layout/item_original_detail_character" />
|
||||
app:layout_constraintTop_toBottomOf="@+id/rl_toolbar">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="24dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp">
|
||||
<!-- Cover small card -->
|
||||
<ImageView
|
||||
android:id="@+id/iv_cover"
|
||||
android:layout_width="168dp"
|
||||
android:layout_height="0dp"
|
||||
android:contentDescription="@null"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintDimensionRatio="306:432"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@drawable/ic_logo_service_center" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="40dp"
|
||||
android:fontFamily="@font/pretendard_bold"
|
||||
android:gravity="center"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="26sp"
|
||||
tools:text="작품 제목" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/ll_meta"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="14dp"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_content_type"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/bg_round_corner_4_263238_ffffff"
|
||||
android:fontFamily="@font/pretendard_regular"
|
||||
android:paddingHorizontal="7dp"
|
||||
android:paddingVertical="3dp"
|
||||
android:textColor="#B0BEC5"
|
||||
android:textSize="14sp"
|
||||
tools:text="웹소설" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_category"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:background="@drawable/bg_round_corner_4_263238_3bb9f1"
|
||||
android:fontFamily="@font/pretendard_regular"
|
||||
android:paddingHorizontal="7dp"
|
||||
android:paddingVertical="3dp"
|
||||
android:textColor="#3bb9f1"
|
||||
android:textSize="14sp"
|
||||
tools:text="로맨스" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_adult"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:background="@drawable/bg_round_corner_4_263238_ff5c49"
|
||||
android:fontFamily="@font/pretendard_regular"
|
||||
android:paddingHorizontal="7dp"
|
||||
android:paddingVertical="3dp"
|
||||
android:text="19+"
|
||||
android:textColor="#FF5C49"
|
||||
android:textSize="14sp"
|
||||
android:visibility="gone" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_tags"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="14dp"
|
||||
android:ellipsize="end"
|
||||
android:fontFamily="@font/pretendard_regular"
|
||||
android:gravity="center"
|
||||
android:textColor="@color/color_3bb9f1"
|
||||
android:textSize="14sp"
|
||||
tools:text="#태그1 #태그2" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/tabs"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="50dp"
|
||||
android:background="@color/black"
|
||||
app:tabIndicatorColor="@color/color_3bb9f1"
|
||||
app:tabIndicatorFullWidth="true"
|
||||
app:tabIndicatorHeight="4dp"
|
||||
app:tabSelectedTextColor="@color/color_3bb9f1"
|
||||
app:tabTextAppearance="@style/tabText"
|
||||
app:tabTextColor="@color/color_777777" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="@color/color_88909090" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
15
app/src/main/res/layout/fragment_original_work_character.xml
Normal file
15
app/src/main/res/layout/fragment_original_work_character.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/black">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rv_character"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipToPadding="false"
|
||||
android:nestedScrollingEnabled="false"
|
||||
tools:listitem="@layout/item_original_detail_character" />
|
||||
</LinearLayout>
|
||||
186
app/src/main/res/layout/fragment_original_work_info.xml
Normal file
186
app/src/main/res/layout/fragment_original_work_info.xml
Normal file
@@ -0,0 +1,186 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout 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"
|
||||
android:background="@color/black"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/bg_round_corner_16_263238"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="@font/pretendard_bold"
|
||||
android:text="작품 소개"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<io.github.glailton.expandabletextview.ExpandableTextView
|
||||
android:id="@+id/tv_desc"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:fontFamily="@font/pretendard_regular"
|
||||
android:textColor="#B0BEC5"
|
||||
android:textSize="14sp"
|
||||
app:animDuration="500"
|
||||
app:collapsedLines="3"
|
||||
app:ellipsizeTextColor="@color/white"
|
||||
app:expandType="layout"
|
||||
app:isExpanded="false"
|
||||
app:readLessText="간략히"
|
||||
app:readMoreText="전체보기"
|
||||
app:textMode="line"
|
||||
tools:text="특별한 꽃을 길러낼 수 있는 능력을 가진 리엘라.\n그녀는 호손 공작의 상속자가 되고 말아버리는데...\n생각하지도 못했던 상속에 당황한 리엘라의 앞에 왕의 동생이자 보석술사인 하운 대공이 나타난다.\n바로 그녀의 특별한 ‘능력’ 때문에!\n\n꽃집 소녀 리엘라의 우당탕탕 공작 상속기!\n두 명의 상속인" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/ll_original_link"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@drawable/bg_round_corner_16_263238"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="@font/pretendard_bold"
|
||||
android:text="원작 보러 가기"
|
||||
android:textColor="#B0BEC5"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<HorizontalScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:scrollbars="none">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/ll_original_links"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal" />
|
||||
</HorizontalScrollView>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@drawable/bg_round_corner_16_263238"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="@font/pretendard_bold"
|
||||
android:text="상세 정보"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/cl_detail"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp">
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/barrier_labels_end"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="end"
|
||||
app:constraint_referenced_ids="tv_label_writer,tv_label_studio,tv_label_original" />
|
||||
|
||||
<!-- 작가 라벨/내용 -->
|
||||
<TextView
|
||||
android:id="@+id/tv_label_writer"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="@font/pretendard_regular"
|
||||
android:text="작가"
|
||||
android:textColor="#B0BEC5"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_writer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:fontFamily="@font/pretendard_regular"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/tv_label_writer"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/barrier_labels_end"
|
||||
app:layout_constraintTop_toTopOf="@id/tv_label_writer"
|
||||
tools:text="writer" />
|
||||
|
||||
<!-- 제작사 라벨/내용 -->
|
||||
<TextView
|
||||
android:id="@+id/tv_label_studio"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:fontFamily="@font/pretendard_regular"
|
||||
android:text="제작사"
|
||||
android:textColor="#B0BEC5"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_label_writer" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_studio"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:fontFamily="@font/pretendard_regular"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/tv_label_studio"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/barrier_labels_end"
|
||||
app:layout_constraintTop_toTopOf="@id/tv_label_studio"
|
||||
tools:text="studio" />
|
||||
|
||||
<!-- 원작 라벨/내용 -->
|
||||
<TextView
|
||||
android:id="@+id/tv_label_original"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:fontFamily="@font/pretendard_regular"
|
||||
android:text="원작"
|
||||
android:textColor="#B0BEC5"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_label_studio" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_original_work"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:fontFamily="@font/pretendard_regular"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/tv_label_original"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/barrier_labels_end"
|
||||
app:layout_constraintTop_toTopOf="@id/tv_label_original"
|
||||
tools:text="original work" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
@@ -1,113 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout 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"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="24dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp">
|
||||
<!-- Cover small card -->
|
||||
<ImageView
|
||||
android:id="@+id/iv_cover"
|
||||
android:layout_width="168dp"
|
||||
android:layout_height="0dp"
|
||||
android:contentDescription="@null"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintDimensionRatio="306:432"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@drawable/ic_logo_service_center" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="40dp"
|
||||
android:fontFamily="@font/pretendard_bold"
|
||||
android:gravity="center"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="26sp"
|
||||
tools:text="작품 제목" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/ll_meta"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="14dp"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_content_type"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/bg_round_corner_4_263238_ffffff"
|
||||
android:fontFamily="@font/pretendard_regular"
|
||||
android:paddingHorizontal="7dp"
|
||||
android:paddingVertical="3dp"
|
||||
android:textColor="#B0BEC5"
|
||||
android:textSize="14sp"
|
||||
tools:text="웹소설" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_category"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:background="@drawable/bg_round_corner_4_263238_3bb9f1"
|
||||
android:fontFamily="@font/pretendard_regular"
|
||||
android:paddingHorizontal="7dp"
|
||||
android:paddingVertical="3dp"
|
||||
android:textColor="#3bb9f1"
|
||||
android:textSize="14sp"
|
||||
tools:text="로맨스" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_adult"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:background="@drawable/bg_round_corner_4_263238_ff5c49"
|
||||
android:fontFamily="@font/pretendard_regular"
|
||||
android:paddingHorizontal="7dp"
|
||||
android:paddingVertical="3dp"
|
||||
android:text="19+"
|
||||
android:textColor="#FF5C49"
|
||||
android:textSize="14sp"
|
||||
android:visibility="gone" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="14dp"
|
||||
android:ellipsize="end"
|
||||
android:fontFamily="@font/pretendard_regular"
|
||||
android:maxLines="2"
|
||||
android:textColor="#CFD8DC"
|
||||
android:textSize="14sp"
|
||||
tools:text="작품 소개 텍스트가 표시됩니다." />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_open_original"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:background="@drawable/bg_round_corner_8_transparent_3bb9f1"
|
||||
android:fontFamily="@font/pretendard_bold"
|
||||
android:gravity="center"
|
||||
android:paddingVertical="15dp"
|
||||
android:text="원작 보러가기"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="@color/color_3bb9f1"
|
||||
android:textSize="16sp" />
|
||||
</LinearLayout>
|
||||
Reference in New Issue
Block a user