diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 199855442..f87f0f070 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -74,9 +74,6 @@ android {
}
}
-// 룸 디비 제거
-// 좋아요 기능 좀더 생각해보기
-
dependencies {
// 프로젝트 의존성
implementation(projects.core.resource)
@@ -131,7 +128,7 @@ dependencies {
// UI 관련 유틸 라이브러리
implementation(libs.dots.indicator) // ViewPager2 indicator
implementation(libs.lottie) // Lottie 애니메이션
- implementation(libs.pull.to.refresh) // Pull 새로고침
+ implementation(libs.swipe.refresh.layout) // Pull 새로고침
// Third-party SDK
implementation(libs.kakao) // 카카오 로그인 API
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f87ca51cd..a67f2cedf 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -96,6 +96,10 @@
android:name=".ui.novelRating.NovelRatingActivity"
android:exported="false"
android:screenOrientation="portrait" />
+
- UserInterestFeedsEntity.UserInterestFeedEntity(
- avatarImage = feed.avatarImage,
- feedContent = feed.feedContent,
- nickname = feed.nickname,
- novelId = feed.novelId,
- novelImage = feed.novelImage,
- novelRating = feed.novelRating,
- novelRatingCount = feed.novelRatingCount,
- novelTitle = feed.novelTitle,
- )
- },
- message = message,
- )
diff --git a/app/src/main/java/com/into/websoso/data/model/UserInterestFeedsEntity.kt b/app/src/main/java/com/into/websoso/data/model/UserInterestFeedsEntity.kt
deleted file mode 100644
index 7d08b5b24..000000000
--- a/app/src/main/java/com/into/websoso/data/model/UserInterestFeedsEntity.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-package com.into.websoso.data.model
-
-data class UserInterestFeedsEntity(
- val userInterestFeeds: List,
- val message: String,
-) {
- data class UserInterestFeedEntity(
- val avatarImage: String,
- val feedContent: String,
- val nickname: String,
- val novelId: Int,
- val novelImage: String,
- val novelRating: Double,
- val novelRatingCount: Int,
- val novelTitle: String,
- )
-}
-
-enum class UserInterestFeedMessage(
- val message: String,
-) {
- NO_INTEREST_NOVELS("NO_INTEREST_NOVELS"),
- NO_ASSOCIATED_FEEDS("NO_ASSOCIATED_FEEDS"),
- ;
-
- companion object {
- fun fromMessage(message: String): UserInterestFeedMessage? = entries.find { it.message == message }
- }
-}
diff --git a/app/src/main/java/com/into/websoso/data/remote/api/FeedApi.kt b/app/src/main/java/com/into/websoso/data/remote/api/FeedApi.kt
index 4c7a1f6eb..31986305c 100644
--- a/app/src/main/java/com/into/websoso/data/remote/api/FeedApi.kt
+++ b/app/src/main/java/com/into/websoso/data/remote/api/FeedApi.kt
@@ -5,7 +5,6 @@ import com.into.websoso.data.remote.response.CommentsResponseDto
import com.into.websoso.data.remote.response.FeedDetailResponseDto
import com.into.websoso.data.remote.response.FeedsResponseDto
import com.into.websoso.data.remote.response.PopularFeedsResponseDto
-import com.into.websoso.data.remote.response.UserInterestFeedsResponseDto
import okhttp3.MultipartBody
import retrofit2.http.Body
import retrofit2.http.DELETE
@@ -47,9 +46,6 @@ interface FeedApi {
@GET("feeds/popular")
suspend fun getPopularFeeds(): PopularFeedsResponseDto
- @GET("feeds/interest")
- suspend fun getUserInterestFeeds(): UserInterestFeedsResponseDto
-
@DELETE("feeds/{feedId}")
suspend fun deleteFeed(
@Path("feedId") feedId: Long,
diff --git a/app/src/main/java/com/into/websoso/data/remote/response/UserInterestFeedsResponseDto.kt b/app/src/main/java/com/into/websoso/data/remote/response/UserInterestFeedsResponseDto.kt
deleted file mode 100644
index af2615c75..000000000
--- a/app/src/main/java/com/into/websoso/data/remote/response/UserInterestFeedsResponseDto.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-package com.into.websoso.data.remote.response
-
-import kotlinx.serialization.SerialName
-import kotlinx.serialization.Serializable
-
-@Serializable
-data class UserInterestFeedsResponseDto(
- @SerialName("recommendFeeds")
- val userInterestFeeds: List,
- @SerialName("message")
- val message: String,
-) {
- @Serializable
- data class UserInterestFeedResponseDto(
- @SerialName("avatarImage")
- val avatarImage: String,
- @SerialName("feedContent")
- val feedContent: String,
- @SerialName("nickname")
- val nickname: String,
- @SerialName("novelId")
- val novelId: Int,
- @SerialName("novelImage")
- val novelImage: String,
- @SerialName("novelRating")
- val novelRating: Double,
- @SerialName("novelRatingCount")
- val novelRatingCount: Int,
- @SerialName("novelTitle")
- val novelTitle: String,
- )
-}
diff --git a/app/src/main/java/com/into/websoso/data/repository/FeedRepository.kt b/app/src/main/java/com/into/websoso/data/repository/FeedRepository.kt
index 54040827c..fefadad96 100644
--- a/app/src/main/java/com/into/websoso/data/repository/FeedRepository.kt
+++ b/app/src/main/java/com/into/websoso/data/repository/FeedRepository.kt
@@ -1,11 +1,9 @@
package com.into.websoso.data.repository
-import com.into.websoso.data.mapper.MultiPartMapper
import com.into.websoso.data.mapper.toData
import com.into.websoso.data.model.FeedEntity
import com.into.websoso.data.model.FeedsEntity
import com.into.websoso.data.model.PopularFeedsEntity
-import com.into.websoso.data.model.UserInterestFeedsEntity
import com.into.websoso.data.remote.api.FeedApi
import javax.inject.Inject
import javax.inject.Singleton
@@ -37,8 +35,6 @@ class FeedRepository
suspend fun fetchPopularFeeds(): PopularFeedsEntity = feedApi.getPopularFeeds().toData()
- suspend fun fetchUserInterestFeeds(): UserInterestFeedsEntity = feedApi.getUserInterestFeeds().toData()
-
suspend fun saveRemovedFeed(feedId: Long) {
runCatching {
feedApi.deleteFeed(feedId)
diff --git a/app/src/main/java/com/into/websoso/ui/detailExplore/DetailExploreActivity.kt b/app/src/main/java/com/into/websoso/ui/detailExplore/DetailExploreActivity.kt
new file mode 100644
index 000000000..1a55a3e19
--- /dev/null
+++ b/app/src/main/java/com/into/websoso/ui/detailExplore/DetailExploreActivity.kt
@@ -0,0 +1,72 @@
+package com.into.websoso.ui.detailExplore
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.activity.viewModels
+import androidx.appcompat.app.AppCompatActivity
+import com.into.websoso.core.common.util.setupSystemBarIconColor
+import com.into.websoso.core.designsystem.theme.WebsosoTheme
+import com.into.websoso.core.resource.R.string.inquire_link
+import com.into.websoso.ui.detailExploreResult.DetailExploreResultActivity
+import com.into.websoso.ui.detailExploreResult.model.DetailExploreFilteredModel
+import dagger.hilt.android.AndroidEntryPoint
+
+@AndroidEntryPoint
+class DetailExploreActivity : AppCompatActivity() {
+ private val detailExploreViewModel: DetailExploreViewModel by viewModels()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ setupSystemBarIconColor(true)
+
+ setContent {
+ WebsosoTheme {
+ DetailExploreScreen(
+ viewModel = detailExploreViewModel,
+ onBackClick = ::finish,
+ onSearchClick = ::navigateToSearchResult,
+ onKeywordInquireClick = ::navigateToKeywordInquire,
+ )
+ }
+ }
+ }
+
+ private fun navigateToSearchResult() {
+ val selectedGenres = detailExploreViewModel.selectedGenres.value ?: emptyList()
+ val isCompleted = detailExploreViewModel.selectedStatus.value?.isCompleted
+ val novelRating = detailExploreViewModel.selectedRating.value
+
+ val keywordIds = detailExploreViewModel.uiState.value
+ ?.categories
+ ?.flatMap { it.keywords.asSequence() }
+ ?.filter { it.isSelected }
+ ?.map { it.keywordId }
+ ?.toList() ?: emptyList()
+
+ startActivity(
+ DetailExploreResultActivity.getIntent(
+ context = this,
+ detailExploreFilteredModel = DetailExploreFilteredModel(
+ genres = selectedGenres,
+ isCompleted = isCompleted,
+ novelRating = novelRating,
+ keywordIds = keywordIds,
+ ),
+ ),
+ )
+ }
+
+ private fun navigateToKeywordInquire() {
+ val intent = Intent(Intent.ACTION_VIEW, Uri.parse(getString(inquire_link)))
+ startActivity(intent)
+ }
+
+ companion object {
+ fun getIntent(context: Context): Intent = Intent(context, DetailExploreActivity::class.java)
+ }
+}
diff --git a/app/src/main/java/com/into/websoso/ui/detailExplore/DetailExploreDialogBottomSheet.kt b/app/src/main/java/com/into/websoso/ui/detailExplore/DetailExploreDialogBottomSheet.kt
deleted file mode 100644
index 3417030fc..000000000
--- a/app/src/main/java/com/into/websoso/ui/detailExplore/DetailExploreDialogBottomSheet.kt
+++ /dev/null
@@ -1,154 +0,0 @@
-package com.into.websoso.ui.detailExplore
-
-import android.os.Bundle
-import android.view.View
-import androidx.core.view.isVisible
-import androidx.fragment.app.activityViewModels
-import androidx.fragment.app.commit
-import com.google.android.material.bottomsheet.BottomSheetBehavior
-import com.google.android.material.bottomsheet.BottomSheetDialog
-import com.into.websoso.R
-import com.into.websoso.core.common.ui.base.BaseBottomSheetDialog
-import com.into.websoso.core.common.util.SingleEventHandler
-import com.into.websoso.databinding.DialogDetailExploreBinding
-import com.into.websoso.ui.detailExplore.info.DetailExploreInfoFragment
-import com.into.websoso.ui.detailExplore.keyword.DetailExploreKeywordFragment
-import com.into.websoso.ui.detailExplore.model.SelectedFragmentTitle
-import com.into.websoso.ui.detailExplore.model.SelectedFragmentTitle.INFO
-import com.into.websoso.ui.detailExplore.model.SelectedFragmentTitle.KEYWORD
-import dagger.hilt.android.AndroidEntryPoint
-
-@AndroidEntryPoint
-class DetailExploreDialogBottomSheet : BaseBottomSheetDialog(R.layout.dialog_detail_explore) {
- private val detailExploreInfoFragment: DetailExploreInfoFragment by lazy { DetailExploreInfoFragment() }
- private val detailExploreKeywordFragment: DetailExploreKeywordFragment by lazy { DetailExploreKeywordFragment() }
- private val detailExploreViewModel: DetailExploreViewModel by activityViewModels()
- private val singleEventHandler: SingleEventHandler by lazy { SingleEventHandler.from() }
-
- override fun onViewCreated(
- view: View,
- savedInstanceState: Bundle?,
- ) {
- super.onViewCreated(view, savedInstanceState)
-
- setupBottomSheet()
- initDetailExploreFragment()
- onReplaceFragmentButtonClick()
- onBottomSheetExitButtonClick()
- setupObserver()
- }
-
- private fun setupBottomSheet() {
- (dialog as BottomSheetDialog).apply {
- behavior.state = BottomSheetBehavior.STATE_EXPANDED
- behavior.isDraggable = false
- behavior.isHideable = false
- setCancelable(false)
- }
- }
-
- private fun initDetailExploreFragment() {
- childFragmentManager.commit {
- add(
- R.id.fcv_detail_explore,
- detailExploreInfoFragment,
- )
- }
- }
-
- private fun onReplaceFragmentButtonClick() {
- binding.tvDetailExploreInfoButton.setOnClickListener {
- singleEventHandler.throttleFirst {
- switchFragment(INFO)
- detailExploreViewModel.updateIsSearchKeywordProceeding(false)
- }
- }
-
- binding.tvDetailExploreKeywordButton.setOnClickListener {
- singleEventHandler.throttleFirst {
- switchFragment(KEYWORD)
- }
- }
- }
-
- private fun switchFragment(selectedFragmentTitle: SelectedFragmentTitle) {
- val fragmentToShow = when (selectedFragmentTitle) {
- INFO -> detailExploreInfoFragment
- KEYWORD -> {
- if (childFragmentManager.findFragmentById(R.id.fcv_detail_explore) !is DetailExploreKeywordFragment) {
- childFragmentManager.commit {
- add(R.id.fcv_detail_explore, detailExploreKeywordFragment)
- }
- }
- detailExploreKeywordFragment
- }
- }
-
- val fragmentToHide = when (selectedFragmentTitle) {
- INFO -> detailExploreKeywordFragment
- KEYWORD -> detailExploreInfoFragment
- }
-
- childFragmentManager.commit {
- setReorderingAllowed(true)
- show(fragmentToShow)
- hide(fragmentToHide)
- }
-
- updateButtonColors(selectedFragmentTitle)
- }
-
- private fun updateButtonColors(selectedFragmentTitle: SelectedFragmentTitle) {
- val selectedColor = requireContext().getColor(R.color.primary_100_6A5DFD)
- val defaultColor = requireContext().getColor(R.color.gray_200_949399)
-
- when (selectedFragmentTitle) {
- INFO -> {
- binding.apply {
- tvDetailExploreInfoButton.setTextColor(selectedColor)
- tvDetailExploreKeywordButton.setTextColor(defaultColor)
- viewDetailExploreSelectedInfoTab.isVisible = true
- viewDetailExploreSelectedKeywordTab.isVisible = false
- }
- }
-
- KEYWORD -> {
- binding.apply {
- tvDetailExploreKeywordButton.setTextColor(selectedColor)
- tvDetailExploreInfoButton.setTextColor(defaultColor)
- viewDetailExploreSelectedInfoTab.isVisible = false
- viewDetailExploreSelectedKeywordTab.isVisible = true
- }
- }
- }
- }
-
- private fun onBottomSheetExitButtonClick() {
- binding.ivDetailExploreExitButton.setOnClickListener {
- detailExploreViewModel.updateSelectedInfoValueClear()
- detailExploreViewModel.updateSelectedKeywordValueClear()
- dismiss()
- }
- }
-
- private fun setupObserver() {
- detailExploreViewModel.isInfoChipSelected.observe(viewLifecycleOwner) { isVisible ->
- binding.ivDetailExploreInfoActiveDot.isVisible = isVisible
- }
-
- detailExploreViewModel.isKeywordChipSelected.observe(viewLifecycleOwner) { isVisible ->
- binding.ivDetailExploreKeywordActiveDot.isVisible = isVisible
- }
- }
-
- override fun onPause() {
- detailExploreViewModel.updateSelectedInfoValueClear()
- detailExploreViewModel.updateSelectedKeywordValueClear()
- dismiss()
- super.onPause()
- }
-
- companion object {
- fun newInstance(): DetailExploreDialogBottomSheet = DetailExploreDialogBottomSheet()
- }
-}
diff --git a/app/src/main/java/com/into/websoso/ui/detailExplore/DetailExploreScreen.kt b/app/src/main/java/com/into/websoso/ui/detailExplore/DetailExploreScreen.kt
new file mode 100644
index 000000000..cc7e7172b
--- /dev/null
+++ b/app/src/main/java/com/into/websoso/ui/detailExplore/DetailExploreScreen.kt
@@ -0,0 +1,76 @@
+package com.into.websoso.ui.detailExplore
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.systemBars
+import androidx.compose.foundation.layout.windowInsetsPadding
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import com.into.websoso.core.designsystem.theme.White
+import com.into.websoso.ui.detailExplore.component.DetailExploreAppBar
+import com.into.websoso.ui.detailExplore.component.DetailExploreCtaButton
+import com.into.websoso.ui.detailExplore.component.DetailExploreInfoTab
+import com.into.websoso.ui.detailExplore.component.DetailExploreKeywordTab
+import com.into.websoso.ui.detailExplore.model.SelectedFragmentTitle.INFO
+import com.into.websoso.ui.detailExplore.model.SelectedFragmentTitle.KEYWORD
+
+@Composable
+fun DetailExploreScreen(
+ viewModel: DetailExploreViewModel,
+ onBackClick: () -> Unit,
+ onSearchClick: () -> Unit,
+ onKeywordInquireClick: () -> Unit,
+) {
+ var selectedTab by rememberSaveable { mutableStateOf(INFO) }
+
+ val isInfoChipSelected by viewModel.isInfoChipSelected.observeAsState(false)
+ val isKeywordChipSelected by viewModel.isKeywordChipSelected.observeAsState(false)
+
+ BackHandler(onBack = onBackClick)
+
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(White)
+ .windowInsetsPadding(WindowInsets.systemBars)
+ .padding(top = 10.dp),
+ ) {
+ Column(modifier = Modifier.fillMaxSize()) {
+ DetailExploreAppBar(
+ selectedTab = selectedTab,
+ isInfoChipActive = isInfoChipSelected,
+ isKeywordChipActive = isKeywordChipSelected,
+ onTabSelected = { selectedTab = it },
+ onBackClick = onBackClick,
+ onResetClick = {
+ when (selectedTab) {
+ INFO -> viewModel.updateSelectedInfoValueClear()
+ KEYWORD -> viewModel.updateSelectedKeywordValueClear()
+ }
+ },
+ )
+ Box(modifier = Modifier.weight(1f)) {
+ when (selectedTab) {
+ INFO -> DetailExploreInfoTab(viewModel = viewModel)
+
+ KEYWORD -> DetailExploreKeywordTab(
+ viewModel = viewModel,
+ onKeywordInquireClick = onKeywordInquireClick,
+ )
+ }
+ }
+ DetailExploreCtaButton(onClick = onSearchClick)
+ }
+ }
+}
diff --git a/app/src/main/java/com/into/websoso/ui/detailExplore/DetailExploreViewModel.kt b/app/src/main/java/com/into/websoso/ui/detailExplore/DetailExploreViewModel.kt
index 66472eabe..cb2945b2e 100644
--- a/app/src/main/java/com/into/websoso/ui/detailExplore/DetailExploreViewModel.kt
+++ b/app/src/main/java/com/into/websoso/ui/detailExplore/DetailExploreViewModel.kt
@@ -29,10 +29,15 @@ class DetailExploreViewModel
private val _selectedStatus: MutableLiveData = MutableLiveData()
val selectedStatus: LiveData get() = _selectedStatus
- private val _selectedRating: MutableLiveData = MutableLiveData()
- val selectedRating: LiveData get() = _selectedRating
+ private val _selectedRatingMin: MutableLiveData = MutableLiveData(RATING_MIN)
+ val selectedRatingMin: LiveData get() = _selectedRatingMin
- val ratings: List = listOf(3.5f, 4.0f, 4.5f, 4.8f)
+ private val _selectedRatingMax: MutableLiveData = MutableLiveData(RATING_MAX)
+ val selectedRatingMax: LiveData get() = _selectedRatingMax
+
+ val selectedRating: LiveData = _selectedRatingMin.map { min ->
+ if (min > RATING_MIN) min else null
+ }
private val _isInfoChipSelected: MediatorLiveData = MediatorLiveData(false)
val isInfoChipSelected: LiveData get() = _isInfoChipSelected
@@ -51,7 +56,10 @@ class DetailExploreViewModel
_isInfoChipSelected.addSource(_selectedStatus) {
_isInfoChipSelected.value = isInfoChipSelectedEnabled()
}
- _isInfoChipSelected.addSource(_selectedRating) {
+ _isInfoChipSelected.addSource(_selectedRatingMin) {
+ _isInfoChipSelected.value = isInfoChipSelectedEnabled()
+ }
+ _isInfoChipSelected.addSource(_selectedRatingMax) {
_isInfoChipSelected.value = isInfoChipSelectedEnabled()
}
@@ -61,15 +69,18 @@ class DetailExploreViewModel
private fun isInfoChipSelectedEnabled(): Boolean {
val isGenreChipSelected: Boolean = _selectedGenres.value?.isNotEmpty() == true
val isStatusChipSelected: Boolean = _selectedStatus.value != null
- val isRatingChipSelected: Boolean = _selectedRating.value != null
+ val isRatingRangeNarrowed: Boolean =
+ (_selectedRatingMin.value ?: RATING_MIN) > RATING_MIN ||
+ (_selectedRatingMax.value ?: RATING_MAX) < RATING_MAX
- return isGenreChipSelected || isStatusChipSelected || isRatingChipSelected
+ return isGenreChipSelected || isStatusChipSelected || isRatingRangeNarrowed
}
fun updateSelectedInfoValueClear() {
_selectedGenres.value = mutableListOf()
_selectedStatus.value = null
- _selectedRating.value = null
+ _selectedRatingMin.value = RATING_MIN
+ _selectedRatingMax.value = RATING_MAX
}
fun updateSelectedGenres(genre: Genre) {
@@ -92,8 +103,12 @@ class DetailExploreViewModel
_selectedStatus.value = status
}
- fun updateSelectedRating(rating: Float?) {
- _selectedRating.value = rating
+ fun updateSelectedRatingRange(
+ min: Float,
+ max: Float,
+ ) {
+ _selectedRatingMin.value = min
+ _selectedRatingMax.value = max
}
fun updateKeyword(searchWord: String?) {
@@ -188,4 +203,10 @@ class DetailExploreViewModel
)
}
}
+
+ companion object {
+ const val RATING_MIN = 0.0f
+ const val RATING_MAX = 5.0f
+ const val RATING_STEP = 0.5f
+ }
}
diff --git a/app/src/main/java/com/into/websoso/ui/detailExplore/component/DetailExploreAppBar.kt b/app/src/main/java/com/into/websoso/ui/detailExplore/component/DetailExploreAppBar.kt
new file mode 100644
index 000000000..fd86ffaa4
--- /dev/null
+++ b/app/src/main/java/com/into/websoso/ui/detailExplore/component/DetailExploreAppBar.kt
@@ -0,0 +1,154 @@
+package com.into.websoso.ui.detailExplore.component
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import com.into.websoso.core.common.util.clickableWithoutRipple
+import com.into.websoso.core.designsystem.theme.Gray200
+import com.into.websoso.core.designsystem.theme.Gray300
+import com.into.websoso.core.designsystem.theme.Primary100
+import com.into.websoso.core.designsystem.theme.WebsosoTheme
+import com.into.websoso.core.designsystem.theme.White
+import com.into.websoso.core.resource.R.drawable.ic_detail_explore_reset
+import com.into.websoso.core.resource.R.drawable.ic_navigate_left
+import com.into.websoso.core.resource.R.string.detail_explore_info
+import com.into.websoso.core.resource.R.string.detail_explore_keyword
+import com.into.websoso.core.resource.R.string.detail_explore_reset
+import com.into.websoso.ui.detailExplore.model.SelectedFragmentTitle
+import com.into.websoso.ui.detailExplore.model.SelectedFragmentTitle.INFO
+import com.into.websoso.ui.detailExplore.model.SelectedFragmentTitle.KEYWORD
+
+@Composable
+fun DetailExploreAppBar(
+ selectedTab: SelectedFragmentTitle,
+ isInfoChipActive: Boolean,
+ isKeywordChipActive: Boolean,
+ onTabSelected: (SelectedFragmentTitle) -> Unit,
+ onBackClick: () -> Unit,
+ onResetClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Row(
+ modifier = modifier
+ .background(White)
+ .fillMaxWidth()
+ .height(44.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Spacer(modifier = Modifier.width(6.dp))
+ Box(
+ modifier = Modifier
+ .size(44.dp)
+ .clickableWithoutRipple(onClick = onBackClick),
+ contentAlignment = Alignment.Center,
+ ) {
+ Image(
+ painter = painterResource(id = ic_navigate_left),
+ contentDescription = null,
+ modifier = Modifier.size(24.dp),
+ )
+ }
+ TabLabel(
+ label = stringResource(detail_explore_info),
+ isSelected = selectedTab == INFO,
+ isActive = isInfoChipActive,
+ slotWidth = 68.dp,
+ onClick = { onTabSelected(INFO) },
+ )
+ TabLabel(
+ label = stringResource(detail_explore_keyword),
+ isSelected = selectedTab == KEYWORD,
+ isActive = isKeywordChipActive,
+ slotWidth = 69.dp,
+ onClick = { onTabSelected(KEYWORD) },
+ )
+ Spacer(modifier = Modifier.weight(1f))
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(4.dp),
+ modifier = Modifier
+ .padding(horizontal = 16.dp)
+ .clickableWithoutRipple(onClick = onResetClick),
+ ) {
+ Image(
+ painter = painterResource(id = ic_detail_explore_reset),
+ contentDescription = null,
+ modifier = Modifier.size(14.dp),
+ )
+ Text(
+ text = stringResource(detail_explore_reset),
+ style = WebsosoTheme.typography.title2,
+ color = Gray300,
+ )
+ }
+ }
+}
+
+@Composable
+private fun TabLabel(
+ label: String,
+ isSelected: Boolean,
+ isActive: Boolean,
+ slotWidth: Dp,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Box(
+ modifier = modifier
+ .width(slotWidth)
+ .height(30.dp)
+ .clickableWithoutRipple(onClick = onClick),
+ contentAlignment = Alignment.Center,
+ ) {
+ Row(verticalAlignment = Alignment.Top) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier.width(IntrinsicSize.Max),
+ ) {
+ Text(
+ text = label,
+ style = WebsosoTheme.typography.title1,
+ color = if (isSelected) Primary100 else Gray200,
+ )
+ Spacer(modifier = Modifier.weight(1f))
+ if (isSelected) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(2.dp)
+ .background(Primary100),
+ )
+ }
+ }
+ Spacer(modifier = Modifier.width(3.dp))
+ Box(
+ modifier = Modifier
+ .size(4.dp)
+ .background(
+ color = if (isActive) Primary100 else Color.Transparent,
+ shape = CircleShape,
+ ),
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/com/into/websoso/ui/detailExplore/component/DetailExploreCtaButton.kt b/app/src/main/java/com/into/websoso/ui/detailExplore/component/DetailExploreCtaButton.kt
new file mode 100644
index 000000000..13cbba0ca
--- /dev/null
+++ b/app/src/main/java/com/into/websoso/ui/detailExplore/component/DetailExploreCtaButton.kt
@@ -0,0 +1,52 @@
+package com.into.websoso.ui.detailExplore.component
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.into.websoso.core.common.util.SingleEventHandler
+import com.into.websoso.core.common.util.clickableWithoutRipple
+import com.into.websoso.core.designsystem.theme.Primary100
+import com.into.websoso.core.designsystem.theme.WebsosoTheme
+import com.into.websoso.core.designsystem.theme.White
+import com.into.websoso.core.resource.R.string.detail_explore_search_novel
+
+@Composable
+fun DetailExploreCtaButton(
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ val singleEventHandler = remember { SingleEventHandler.from() }
+ Column(
+ modifier = modifier
+ .fillMaxWidth()
+ .background(White)
+ .padding(horizontal = 8.dp, vertical = 10.dp),
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(14.dp))
+ .background(Primary100)
+ .clickableWithoutRipple { singleEventHandler.throttleFirst(event = onClick) }
+ .padding(vertical = 18.dp),
+ contentAlignment = Alignment.Center,
+ ) {
+ Text(
+ text = stringResource(detail_explore_search_novel),
+ style = WebsosoTheme.typography.title1,
+ color = White,
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/com/into/websoso/ui/detailExplore/component/DetailExploreInfoTab.kt b/app/src/main/java/com/into/websoso/ui/detailExplore/component/DetailExploreInfoTab.kt
new file mode 100644
index 000000000..a39ed8ad4
--- /dev/null
+++ b/app/src/main/java/com/into/websoso/ui/detailExplore/component/DetailExploreInfoTab.kt
@@ -0,0 +1,239 @@
+package com.into.websoso.ui.detailExplore.component
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
+import androidx.compose.foundation.layout.FlowRow
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.into.websoso.core.designsystem.theme.Black
+import com.into.websoso.core.designsystem.theme.Gray50
+import com.into.websoso.core.designsystem.theme.Primary100
+import com.into.websoso.core.designsystem.theme.WebsosoTheme
+import com.into.websoso.core.resource.R.string.detail_explore_info_genre
+import com.into.websoso.core.resource.R.string.detail_explore_info_rating
+import com.into.websoso.core.resource.R.string.detail_explore_info_rating_range
+import com.into.websoso.core.resource.R.string.detail_explore_info_rating_value
+import com.into.websoso.core.resource.R.string.detail_explore_info_status
+import com.into.websoso.core.resource.R.string.detail_explore_info_status_complete
+import com.into.websoso.core.resource.R.string.detail_explore_info_status_in_series
+import com.into.websoso.ui.detailExplore.DetailExploreViewModel
+import com.into.websoso.ui.detailExplore.DetailExploreViewModel.Companion.RATING_MAX
+import com.into.websoso.ui.detailExplore.DetailExploreViewModel.Companion.RATING_MIN
+import com.into.websoso.ui.detailExplore.DetailExploreViewModel.Companion.RATING_STEP
+import com.into.websoso.ui.detailExplore.info.model.Genre
+import com.into.websoso.ui.detailExplore.info.model.SeriesStatus
+
+@Composable
+fun DetailExploreInfoTab(
+ viewModel: DetailExploreViewModel,
+ modifier: Modifier = Modifier,
+) {
+ val selectedGenres by viewModel.selectedGenres.observeAsState(emptyList())
+ val selectedStatus by viewModel.selectedStatus.observeAsState(null)
+ val ratingMin by viewModel.selectedRatingMin.observeAsState(RATING_MIN)
+ val ratingMax by viewModel.selectedRatingMax.observeAsState(RATING_MAX)
+
+ Column(
+ modifier = modifier
+ .fillMaxWidth()
+ .verticalScroll(rememberScrollState()),
+ verticalArrangement = Arrangement.spacedBy(32.dp),
+ ) {
+ GenreSection(
+ selectedGenres = selectedGenres,
+ onGenreClick = viewModel::updateSelectedGenres,
+ )
+ StatusSection(
+ selectedStatus = selectedStatus,
+ onStatusClick = { status ->
+ viewModel.updateSelectedSeriesStatus(
+ if (selectedStatus == status) null else status,
+ )
+ },
+ )
+ RatingSection(
+ min = ratingMin,
+ max = ratingMax,
+ onRangeChange = viewModel::updateSelectedRatingRange,
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ }
+}
+
+@Composable
+private fun SectionTitle(text: String) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 20.dp, vertical = 10.dp),
+ ) {
+ Text(
+ text = text,
+ style = WebsosoTheme.typography.title2,
+ color = Black,
+ )
+ }
+}
+
+@OptIn(ExperimentalLayoutApi::class)
+@Composable
+private fun GenreSection(
+ selectedGenres: List,
+ onGenreClick: (Genre) -> Unit,
+) {
+ Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
+ SectionTitle(text = stringResource(detail_explore_info_genre))
+ FlowRow(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 20.dp),
+ horizontalArrangement = Arrangement.spacedBy(6.dp),
+ verticalArrangement = Arrangement.spacedBy(14.dp),
+ ) {
+ Genre.entries.forEach { genre ->
+ SelectableTagChip(
+ label = genre.titleKr,
+ isSelected = selectedGenres.contains(genre),
+ onClick = { onGenreClick(genre) },
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun StatusSection(
+ selectedStatus: SeriesStatus?,
+ onStatusClick: (SeriesStatus) -> Unit,
+) {
+ Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
+ SectionTitle(text = stringResource(detail_explore_info_status))
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 20.dp),
+ horizontalArrangement = Arrangement.spacedBy(10.dp),
+ ) {
+ StatusChipCell(
+ label = stringResource(detail_explore_info_status_in_series),
+ status = SeriesStatus.IN_SERIES,
+ selectedStatus = selectedStatus,
+ onClick = onStatusClick,
+ modifier = Modifier.weight(1f),
+ )
+ StatusChipCell(
+ label = stringResource(detail_explore_info_status_complete),
+ status = SeriesStatus.COMPLETED,
+ selectedStatus = selectedStatus,
+ onClick = onStatusClick,
+ modifier = Modifier.weight(1f),
+ )
+ }
+ }
+}
+
+@Composable
+private fun StatusChipCell(
+ label: String,
+ status: SeriesStatus,
+ selectedStatus: SeriesStatus?,
+ onClick: (SeriesStatus) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ SelectableStatusChip(
+ label = label,
+ isSelected = selectedStatus == status,
+ onClick = { onClick(status) },
+ modifier = modifier.wrapContentHeight(),
+ )
+}
+
+@Composable
+private fun RatingSection(
+ min: Float,
+ max: Float,
+ onRangeChange: (Float, Float) -> Unit,
+) {
+ Column {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(42.dp)
+ .padding(horizontal = 20.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Text(
+ text = stringResource(detail_explore_info_rating),
+ style = WebsosoTheme.typography.title2,
+ color = Black,
+ )
+ Spacer(modifier = Modifier.weight(1f))
+ Text(
+ text = stringResource(detail_explore_info_rating_range, min, max),
+ style = WebsosoTheme.typography.body2,
+ color = Primary100,
+ )
+ }
+ Spacer(modifier = Modifier.height(6.dp))
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(38.dp)
+ .padding(horizontal = 20.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(10.dp),
+ ) {
+ RatingValueBox(value = min)
+ Box(modifier = Modifier.weight(1f)) {
+ RatingRangeSlider(
+ min = min,
+ max = max,
+ valueRange = RATING_MIN..RATING_MAX,
+ stepSize = RATING_STEP,
+ onValueChange = onRangeChange,
+ )
+ }
+ RatingValueBox(value = max)
+ }
+ Spacer(modifier = Modifier.height(38.dp))
+ }
+}
+
+@Composable
+private fun RatingValueBox(value: Float) {
+ Box(
+ modifier = Modifier
+ .size(width = 50.dp, height = 38.dp)
+ .clip(RoundedCornerShape(8.dp))
+ .background(Gray50),
+ contentAlignment = Alignment.Center,
+ ) {
+ Text(
+ text = stringResource(detail_explore_info_rating_value, value),
+ style = WebsosoTheme.typography.body2,
+ color = Primary100,
+ )
+ }
+}
diff --git a/app/src/main/java/com/into/websoso/ui/detailExplore/component/DetailExploreKeywordTab.kt b/app/src/main/java/com/into/websoso/ui/detailExplore/component/DetailExploreKeywordTab.kt
new file mode 100644
index 000000000..1a5d7261a
--- /dev/null
+++ b/app/src/main/java/com/into/websoso/ui/detailExplore/component/DetailExploreKeywordTab.kt
@@ -0,0 +1,431 @@
+package com.into.websoso.ui.detailExplore.component
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
+import androidx.compose.foundation.layout.FlowRow
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.rotate
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import com.into.websoso.core.common.ui.model.CategoriesModel.CategoryModel
+import com.into.websoso.core.common.ui.model.CategoriesModel.CategoryModel.KeywordModel
+import com.into.websoso.core.common.util.clickableWithoutRipple
+import com.into.websoso.core.common.util.getS3ImageUrl
+import com.into.websoso.core.designsystem.component.NetworkImage
+import com.into.websoso.core.designsystem.theme.Black
+import com.into.websoso.core.designsystem.theme.Gray100
+import com.into.websoso.core.designsystem.theme.Gray200
+import com.into.websoso.core.designsystem.theme.Gray300
+import com.into.websoso.core.designsystem.theme.Gray50
+import com.into.websoso.core.designsystem.theme.Primary100
+import com.into.websoso.core.designsystem.theme.WebsosoTheme
+import com.into.websoso.core.designsystem.theme.White
+import com.into.websoso.core.resource.R.drawable.ic_common_search
+import com.into.websoso.core.resource.R.drawable.ic_common_search_clear
+import com.into.websoso.core.resource.R.drawable.ic_detail_explore_keyword_not_exist_result
+import com.into.websoso.core.resource.R.drawable.ic_novel_rating_keword_remove
+import com.into.websoso.core.resource.R.drawable.ic_novel_rating_keyword_toggle_selected
+import com.into.websoso.core.resource.R.string.detail_explore_keyword_inquire_keyword
+import com.into.websoso.core.resource.R.string.detail_explore_keyword_not_exist_result
+import com.into.websoso.core.resource.R.string.detail_explore_keyword_search_result
+import com.into.websoso.core.resource.R.string.detail_explore_search_hint
+import com.into.websoso.ui.detailExplore.DetailExploreViewModel
+import com.into.websoso.ui.detailExplore.keyword.model.DetailExploreKeywordUiState
+
+@Composable
+fun DetailExploreKeywordTab(
+ viewModel: DetailExploreViewModel,
+ onKeywordInquireClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ val uiState by viewModel.uiState.observeAsState(DetailExploreKeywordUiState())
+ var searchValue by remember { mutableStateOf(TextFieldValue("")) }
+
+ val selectedKeywords = remember(uiState.categories) {
+ uiState.categories.flatMap { it.keywords }.filter { it.isSelected }
+ }
+
+ Column(modifier = modifier.fillMaxWidth()) {
+ KeywordSearchField(
+ value = searchValue,
+ onValueChange = { value ->
+ searchValue = value
+ if (value.text.isEmpty()) {
+ viewModel.initSearchKeyword()
+ } else {
+ viewModel.updateIsSearchKeywordProceeding(true)
+ }
+ },
+ onSearch = {
+ if (searchValue.text.isEmpty()) {
+ viewModel.initSearchKeyword()
+ } else {
+ viewModel.updateKeyword(searchValue.text)
+ }
+ },
+ onClear = {
+ searchValue = TextFieldValue("")
+ viewModel.initSearchKeyword()
+ },
+ )
+
+ if (selectedKeywords.isNotEmpty()) {
+ SelectedKeywordsRow(
+ keywords = selectedKeywords,
+ onRemove = viewModel::updateClickedChipState,
+ )
+ }
+
+ when {
+ uiState.isSearchKeywordProceeding && uiState.isSearchResultKeywordsEmpty -> {
+ KeywordEmptyResult(onInquireClick = onKeywordInquireClick)
+ }
+
+ uiState.isSearchKeywordProceeding -> {
+ Divider()
+ SearchResultList(
+ keywords = uiState.searchResultKeywords,
+ isInitial = uiState.isInitialSearchKeyword,
+ onKeywordClick = viewModel::updateClickedChipState,
+ )
+ }
+
+ else -> {
+ CategoryList(
+ categories = uiState.categories,
+ onKeywordClick = viewModel::updateClickedChipState,
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun KeywordSearchField(
+ value: TextFieldValue,
+ onValueChange: (TextFieldValue) -> Unit,
+ onSearch: () -> Unit,
+ onClear: () -> Unit,
+) {
+ Box(
+ modifier = Modifier
+ .padding(horizontal = 20.dp, vertical = 12.dp)
+ .fillMaxWidth()
+ .height(44.dp)
+ .clip(RoundedCornerShape(14.dp))
+ .background(Gray50)
+ .padding(horizontal = 14.dp),
+ contentAlignment = Alignment.CenterStart,
+ ) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Box(modifier = Modifier.weight(1f)) {
+ BasicTextField(
+ value = value,
+ onValueChange = onValueChange,
+ singleLine = true,
+ keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
+ keyboardActions = KeyboardActions(onSearch = { onSearch() }),
+ cursorBrush = SolidColor(Primary100),
+ textStyle = WebsosoTheme.typography.body4.copy(color = Black),
+ )
+ if (value.text.isEmpty()) {
+ Text(
+ text = stringResource(detail_explore_search_hint),
+ style = WebsosoTheme.typography.body4,
+ color = Gray200,
+ )
+ }
+ }
+ if (value.text.isNotEmpty()) {
+ androidx.compose.foundation.Image(
+ painter = painterResource(id = ic_common_search_clear),
+ contentDescription = null,
+ modifier = Modifier
+ .size(18.dp)
+ .clickableWithoutRipple(onClick = onClear),
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ }
+ androidx.compose.foundation.Image(
+ painter = painterResource(id = ic_common_search),
+ contentDescription = null,
+ modifier = Modifier
+ .size(20.dp)
+ .clickableWithoutRipple(onClick = onSearch),
+ )
+ }
+ }
+}
+
+@Composable
+private fun SelectedKeywordsRow(
+ keywords: List,
+ onRemove: (Int) -> Unit,
+) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .horizontalScroll(rememberScrollState())
+ .padding(horizontal = 20.dp)
+ .padding(bottom = 8.dp),
+ horizontalArrangement = Arrangement.spacedBy(6.dp),
+ ) {
+ keywords.forEach { keyword ->
+ SelectedKeywordChip(
+ label = keyword.keywordName,
+ onRemove = { onRemove(keyword.keywordId) },
+ )
+ }
+ }
+}
+
+@Composable
+private fun SelectedKeywordChip(
+ label: String,
+ onRemove: () -> Unit,
+) {
+ val shape = RoundedCornerShape(20.dp)
+ Row(
+ modifier = Modifier
+ .clip(shape)
+ .border(width = 1.dp, color = Primary100, shape = shape)
+ .background(color = White, shape = shape)
+ .clickableWithoutRipple(onClick = onRemove)
+ .padding(start = 13.dp, end = 11.dp, top = 7.dp, bottom = 7.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(6.dp),
+ ) {
+ Text(
+ text = label,
+ style = WebsosoTheme.typography.body2,
+ color = Primary100,
+ )
+ androidx.compose.foundation.Image(
+ painter = painterResource(id = ic_novel_rating_keword_remove),
+ contentDescription = null,
+ modifier = Modifier.size(10.dp),
+ )
+ }
+}
+
+@Composable
+private fun Divider() {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(1.dp)
+ .background(Gray50),
+ )
+}
+
+@Composable
+private fun CategoryList(
+ categories: List,
+ onKeywordClick: (Int) -> Unit,
+) {
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(Gray50),
+ contentPadding = PaddingValues(vertical = 12.dp, horizontal = 12.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp),
+ ) {
+ items(items = categories, key = { it.categoryName }) { category ->
+ CategoryCard(category = category, onKeywordClick = onKeywordClick)
+ }
+ }
+}
+
+@OptIn(ExperimentalLayoutApi::class)
+@Composable
+private fun CategoryCard(
+ category: CategoryModel,
+ onKeywordClick: (Int) -> Unit,
+) {
+ var isExpanded by remember { mutableStateOf(false) }
+ val context = LocalContext.current
+ val imageUrl = remember(category.categoryImage) {
+ context.getS3ImageUrl(category.categoryImage)
+ }
+
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(14.dp))
+ .background(White),
+ ) {
+ Row(
+ modifier = Modifier
+ .padding(start = 20.dp, top = 20.dp, end = 20.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ NetworkImage(
+ imageUrl = imageUrl,
+ contentDescription = null,
+ modifier = Modifier
+ .size(28.dp)
+ .clip(RoundedCornerShape(10.dp)),
+ )
+ Text(
+ text = category.categoryName,
+ style = WebsosoTheme.typography.title2,
+ color = Gray300,
+ )
+ }
+ Spacer(modifier = Modifier.height(16.dp))
+ FlowRow(
+ modifier = Modifier
+ .padding(horizontal = 20.dp)
+ .fillMaxWidth()
+ .heightIn(max = if (isExpanded) Int.MAX_VALUE.dp else 92.dp)
+ .clip(RoundedCornerShape(0.dp)),
+ horizontalArrangement = Arrangement.spacedBy(6.dp),
+ verticalArrangement = Arrangement.spacedBy(0.dp),
+ ) {
+ category.keywords.forEach { keyword ->
+ Box(modifier = Modifier.padding(vertical = 4.dp)) {
+ SelectableTagChip(
+ label = keyword.keywordName,
+ isSelected = keyword.isSelected,
+ onClick = { onKeywordClick(keyword.keywordId) },
+ )
+ }
+ }
+ }
+ Spacer(modifier = Modifier.height(18.dp))
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(1.dp)
+ .background(Gray50),
+ )
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(44.dp)
+ .clickableWithoutRipple { isExpanded = !isExpanded },
+ contentAlignment = Alignment.Center,
+ ) {
+ androidx.compose.foundation.Image(
+ painter = painterResource(id = ic_novel_rating_keyword_toggle_selected),
+ contentDescription = null,
+ modifier = Modifier
+ .size(16.dp)
+ .rotate(if (isExpanded) 0f else 180f),
+ )
+ }
+ }
+}
+
+@OptIn(ExperimentalLayoutApi::class)
+@Composable
+private fun SearchResultList(
+ keywords: List,
+ isInitial: Boolean,
+ onKeywordClick: (Int) -> Unit,
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .verticalScroll(rememberScrollState())
+ .padding(horizontal = 20.dp),
+ ) {
+ if (!isInitial) {
+ Spacer(modifier = Modifier.height(28.dp))
+ Text(
+ text = stringResource(detail_explore_keyword_search_result),
+ style = WebsosoTheme.typography.title3,
+ color = Gray300,
+ )
+ Spacer(modifier = Modifier.height(20.dp))
+ }
+ FlowRow(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(6.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ keywords.forEach { keyword ->
+ SelectableTagChip(
+ label = keyword.keywordName,
+ isSelected = keyword.isSelected,
+ onClick = { onKeywordClick(keyword.keywordId) },
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun KeywordEmptyResult(onInquireClick: () -> Unit) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 80.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ androidx.compose.foundation.Image(
+ painter = painterResource(id = ic_detail_explore_keyword_not_exist_result),
+ contentDescription = null,
+ modifier = Modifier.size(80.dp),
+ )
+ Spacer(modifier = Modifier.height(12.dp))
+ Text(
+ text = stringResource(detail_explore_keyword_not_exist_result),
+ style = WebsosoTheme.typography.body1,
+ color = Gray200,
+ textAlign = TextAlign.Center,
+ )
+ Spacer(modifier = Modifier.height(36.dp))
+ Box(
+ modifier = Modifier
+ .clip(RoundedCornerShape(14.dp))
+ .background(Gray50)
+ .clickableWithoutRipple(onClick = onInquireClick)
+ .padding(horizontal = 26.dp, vertical = 20.dp),
+ ) {
+ Text(
+ text = stringResource(detail_explore_keyword_inquire_keyword),
+ style = WebsosoTheme.typography.title2,
+ color = Primary100,
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/com/into/websoso/ui/detailExplore/component/RatingRangeSlider.kt b/app/src/main/java/com/into/websoso/ui/detailExplore/component/RatingRangeSlider.kt
new file mode 100644
index 000000000..240115325
--- /dev/null
+++ b/app/src/main/java/com/into/websoso/ui/detailExplore/component/RatingRangeSlider.kt
@@ -0,0 +1,163 @@
+package com.into.websoso.ui.detailExplore.component
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.detectDragGestures
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.dp
+import com.into.websoso.core.designsystem.theme.Primary100
+import com.into.websoso.core.designsystem.theme.Primary50
+import com.into.websoso.core.designsystem.theme.White
+import kotlin.math.absoluteValue
+import kotlin.math.round
+
+@Composable
+fun RatingRangeSlider(
+ min: Float,
+ max: Float,
+ valueRange: ClosedFloatingPointRange,
+ stepSize: Float,
+ onValueChange: (Float, Float) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ val totalRange = valueRange.endInclusive - valueRange.start
+ val tickCount = ((totalRange / stepSize).toInt() + 1).coerceAtLeast(2)
+
+ val density = LocalDensity.current
+ val thumbRadiusPx = with(density) { 8.dp.toPx() }
+
+ var sliderWidthPx by remember { mutableFloatStateOf(0f) }
+ var activeThumb by remember { mutableIntStateOf(0) }
+
+ val trackWidthPx = (sliderWidthPx - thumbRadiusPx * 2).coerceAtLeast(0f)
+ val startFraction = ((min - valueRange.start) / totalRange).coerceIn(0f, 1f)
+ val endFraction = ((max - valueRange.start) / totalRange).coerceIn(0f, 1f)
+ val startCenterPx = thumbRadiusPx + trackWidthPx * startFraction
+ val endCenterPx = thumbRadiusPx + trackWidthPx * endFraction
+
+ val latestMin by rememberUpdatedState(min)
+ val latestMax by rememberUpdatedState(max)
+ val latestStartCenter by rememberUpdatedState(startCenterPx)
+ val latestEndCenter by rememberUpdatedState(endCenterPx)
+
+ fun snap(rawValue: Float): Float {
+ val stepped = round(rawValue / stepSize) * stepSize
+ return stepped.coerceIn(valueRange.start, valueRange.endInclusive)
+ }
+
+ fun valueAtX(x: Float): Float {
+ if (trackWidthPx <= 0f) return valueRange.start
+ val fraction = ((x - thumbRadiusPx) / trackWidthPx).coerceIn(0f, 1f)
+ return snap(valueRange.start + fraction * totalRange)
+ }
+
+ Box(
+ modifier = modifier
+ .fillMaxWidth()
+ .height(16.dp)
+ .onSizeChanged { sliderWidthPx = it.width.toFloat() }
+ .pointerInput(Unit) {
+ detectDragGestures(
+ onDragStart = { offset ->
+ val touchX = offset.x
+ activeThumb =
+ if ((touchX - latestStartCenter).absoluteValue <=
+ (touchX - latestEndCenter).absoluteValue
+ ) {
+ 1
+ } else {
+ 2
+ }
+ val newValue = valueAtX(touchX)
+ if (activeThumb == 1) {
+ onValueChange(newValue.coerceAtMost(latestMax), latestMax)
+ } else {
+ onValueChange(latestMin, newValue.coerceAtLeast(latestMin))
+ }
+ },
+ onDrag = { change, _ ->
+ change.consume()
+ val newValue = valueAtX(change.position.x)
+ if (activeThumb == 1) {
+ onValueChange(newValue.coerceAtMost(latestMax), latestMax)
+ } else {
+ onValueChange(latestMin, newValue.coerceAtLeast(latestMin))
+ }
+ },
+ )
+ },
+ contentAlignment = Alignment.CenterStart,
+ ) {
+ Box(
+ modifier = Modifier
+ .padding(horizontal = 8.dp)
+ .fillMaxWidth()
+ .height(4.dp)
+ .clip(RoundedCornerShape(6.dp))
+ .background(Primary50),
+ )
+ Row(
+ modifier = Modifier
+ .padding(horizontal = 8.dp)
+ .fillMaxWidth()
+ .height(4.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ repeat(tickCount) {
+ Box(
+ modifier = Modifier
+ .size(width = 1.dp, height = 2.dp)
+ .background(Primary100, RoundedCornerShape(1.dp)),
+ )
+ }
+ }
+ val activeStartDp = with(density) { startCenterPx.toDp() }
+ val activeWidthDp = with(density) { (endCenterPx - startCenterPx).coerceAtLeast(0f).toDp() }
+ Box(
+ modifier = Modifier
+ .offset(x = activeStartDp)
+ .width(activeWidthDp)
+ .height(4.dp)
+ .clip(RoundedCornerShape(6.dp))
+ .background(Primary100),
+ )
+ ThumbCircle(offsetXDp = with(density) { (startCenterPx - thumbRadiusPx).toDp() })
+ ThumbCircle(offsetXDp = with(density) { (endCenterPx - thumbRadiusPx).toDp() })
+ }
+}
+
+@Composable
+private fun ThumbCircle(offsetXDp: androidx.compose.ui.unit.Dp) {
+ Box(
+ modifier = Modifier
+ .offset(x = offsetXDp)
+ .shadow(elevation = 2.dp, shape = CircleShape, clip = false)
+ .size(16.dp)
+ .background(White, CircleShape),
+ )
+}
diff --git a/app/src/main/java/com/into/websoso/ui/detailExplore/component/SelectableChip.kt b/app/src/main/java/com/into/websoso/ui/detailExplore/component/SelectableChip.kt
new file mode 100644
index 000000000..6ba028449
--- /dev/null
+++ b/app/src/main/java/com/into/websoso/ui/detailExplore/component/SelectableChip.kt
@@ -0,0 +1,90 @@
+package com.into.websoso.ui.detailExplore.component
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import com.into.websoso.core.common.util.clickableWithoutRipple
+import com.into.websoso.core.designsystem.theme.Gray300
+import com.into.websoso.core.designsystem.theme.Gray50
+import com.into.websoso.core.designsystem.theme.Primary100
+import com.into.websoso.core.designsystem.theme.Primary50
+import com.into.websoso.core.designsystem.theme.Transparent
+import com.into.websoso.core.designsystem.theme.WebsosoTheme
+
+@Composable
+fun SelectableTagChip(
+ label: String,
+ isSelected: Boolean,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ SelectableChipBase(
+ label = label,
+ isSelected = isSelected,
+ onClick = onClick,
+ cornerRadius = 20.dp,
+ horizontalPadding = 13.dp,
+ verticalPadding = 7.dp,
+ modifier = modifier,
+ )
+}
+
+@Composable
+fun SelectableStatusChip(
+ label: String,
+ isSelected: Boolean,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ SelectableChipBase(
+ label = label,
+ isSelected = isSelected,
+ onClick = onClick,
+ cornerRadius = 8.dp,
+ horizontalPadding = 0.dp,
+ verticalPadding = 10.dp,
+ modifier = modifier,
+ )
+}
+
+@Composable
+private fun SelectableChipBase(
+ label: String,
+ isSelected: Boolean,
+ onClick: () -> Unit,
+ cornerRadius: Dp,
+ horizontalPadding: Dp,
+ verticalPadding: Dp,
+ modifier: Modifier = Modifier,
+) {
+ val shape = RoundedCornerShape(cornerRadius)
+ val background = if (isSelected) Primary50 else Gray50
+ val borderColor = if (isSelected) Primary100 else Transparent
+ val textColor = if (isSelected) Primary100 else Gray300
+
+ Box(
+ modifier = modifier
+ .clip(shape)
+ .clickableWithoutRipple(onClick = onClick)
+ .background(color = background, shape = shape)
+ .border(width = 1.dp, color = borderColor, shape = shape)
+ .padding(horizontal = horizontalPadding, vertical = verticalPadding),
+ contentAlignment = Alignment.Center,
+ ) {
+ Text(
+ text = label,
+ style = WebsosoTheme.typography.body2,
+ color = textColor,
+ )
+ }
+}
diff --git a/app/src/main/java/com/into/websoso/ui/detailExplore/info/DetailExploreInfoFragment.kt b/app/src/main/java/com/into/websoso/ui/detailExplore/info/DetailExploreInfoFragment.kt
deleted file mode 100644
index a2c8d886b..000000000
--- a/app/src/main/java/com/into/websoso/ui/detailExplore/info/DetailExploreInfoFragment.kt
+++ /dev/null
@@ -1,192 +0,0 @@
-package com.into.websoso.ui.detailExplore.info
-
-import android.os.Bundle
-import android.view.View
-import androidx.core.view.forEach
-import androidx.fragment.app.activityViewModels
-import com.google.android.material.chip.Chip
-import com.into.websoso.R
-import com.into.websoso.core.common.ui.base.BaseFragment
-import com.into.websoso.core.common.ui.custom.WebsosoChip
-import com.into.websoso.core.common.util.SingleEventHandler
-import com.into.websoso.core.common.util.toFloatPxFromDp
-import com.into.websoso.databinding.FragmentDetailExploreInfoBinding
-import com.into.websoso.ui.detailExplore.DetailExploreViewModel
-import com.into.websoso.ui.detailExplore.info.model.Genre
-import com.into.websoso.ui.detailExplore.info.model.Rating
-import com.into.websoso.ui.detailExplore.info.model.SeriesStatus
-import com.into.websoso.ui.detailExploreResult.DetailExploreResultActivity
-import com.into.websoso.ui.detailExploreResult.model.DetailExploreFilteredModel
-import dagger.hilt.android.AndroidEntryPoint
-
-@AndroidEntryPoint
-class DetailExploreInfoFragment : BaseFragment(R.layout.fragment_detail_explore_info) {
- private val detailExploreViewModel: DetailExploreViewModel by activityViewModels()
- private val singleEventHandler: SingleEventHandler by lazy { SingleEventHandler.from() }
-
- override fun onViewCreated(
- view: View,
- savedInstanceState: Bundle?,
- ) {
- super.onViewCreated(view, savedInstanceState)
-
- bindViewModel()
- onResetButtonClick()
- onDetailSearchNovelButtonClick()
- setupGenreChips()
- setupSeriesStatusChips()
- setupRatingChips()
- setupObserver()
- }
-
- private fun bindViewModel() {
- binding.detailExploreViewModel = detailExploreViewModel
- binding.lifecycleOwner = this
- }
-
- private fun onResetButtonClick() {
- binding.clDetailExploreInfoResetButton.setOnClickListener {
- detailExploreViewModel.updateSelectedInfoValueClear()
- }
- }
-
- private fun onDetailSearchNovelButtonClick() {
- binding.tvDetailExploreSearchButton.setOnClickListener {
- singleEventHandler.throttleFirst {
- val selectedGenres = detailExploreViewModel.selectedGenres.value ?: emptyList()
- val isCompleted = detailExploreViewModel.selectedStatus.value?.isCompleted
- val novelRating = detailExploreViewModel.selectedRating.value
-
- val keywordIds = detailExploreViewModel.uiState.value
- ?.categories
- ?.flatMap { it.keywords.asSequence() }
- ?.filter { it.isSelected }
- ?.map { it.keywordId }
- ?.toList() ?: emptyList()
-
- val intent = DetailExploreResultActivity.getIntent(
- context = requireContext(),
- DetailExploreFilteredModel(
- genres = selectedGenres,
- isCompleted = isCompleted,
- novelRating = novelRating,
- keywordIds = keywordIds,
- ),
- )
-
- startActivity(intent)
- }
- }
- }
-
- private fun setupGenreChips() {
- val genres = Genre.entries
- genres.forEach { genre ->
- WebsosoChip(requireContext())
- .apply {
- setWebsosoChipText(genre.titleKr)
- setWebsosoChipTextAppearance(R.style.body2)
- setWebsosoChipTextColor(R.color.bg_detail_explore_chip_text_selector)
- setWebsosoChipStrokeColor(R.color.bg_detail_explore_chip_stroke_selector)
- setWebsosoChipBackgroundColor(R.color.bg_detail_explore_chip_background_selector)
- setWebsosoChipPaddingVertical(12f.toFloatPxFromDp())
- setWebsosoChipPaddingHorizontal(6f.toFloatPxFromDp())
- setWebsosoChipRadius(20f.toFloatPxFromDp())
- setOnWebsosoChipClick { detailExploreViewModel.updateSelectedGenres(genre) }
- }.also { websosoChip -> binding.wcgDetailExploreInfoGenre.addChip(websosoChip) }
- }
- }
-
- private fun setupSeriesStatusChips() {
- val seriesStatusChips = listOf(
- binding.chipDetailExploreInfoStatusInSeries,
- binding.chipDetailExploreInfoStatusComplete,
- )
-
- seriesStatusChips.forEach { chip ->
- setupChipCheckListener(chip) { isChecked ->
- when (isChecked) {
- true -> {
- seriesStatusChips.filter { it != chip }.forEach { it.isChecked = false }
-
- val status = SeriesStatus.from(chip.text.toString())
- detailExploreViewModel.updateSelectedSeriesStatus(status)
- }
-
- false -> {
- detailExploreViewModel.updateSelectedSeriesStatus(null)
- }
- }
- }
- }
- }
-
- private fun setupRatingChips() {
- val ratingChips = listOf(
- binding.chipDetailExploreInfoRatingLowest,
- binding.chipDetailExploreInfoRatingLower,
- binding.chipDetailExploreInfoRatingHigher,
- binding.chipDetailExploreInfoRatingHighest,
- )
-
- ratingChips.forEach { chip ->
- setupChipCheckListener(chip) { isChecked ->
- when (isChecked) {
- true -> {
- ratingChips.filter { it != chip }.forEach { it.isChecked = false }
- val ratingValue =
- Rating.entries.find {
- chip.text.toString().contains(it.value.toString())
- }
- detailExploreViewModel.updateSelectedRating(ratingValue?.value)
- }
-
- false -> detailExploreViewModel.updateSelectedRating(null)
- }
- }
- }
- }
-
- private fun setupChipCheckListener(
- chip: Chip,
- onCheckedChange: (Boolean) -> Unit,
- ) {
- chip.setOnCheckedChangeListener(null)
- chip.isChecked = false
- chip.setOnCheckedChangeListener { _, isChecked -> onCheckedChange(isChecked) }
- }
-
- private fun setupObserver() {
- detailExploreViewModel.selectedGenres.observe(viewLifecycleOwner) { genres ->
- if (genres.isNullOrEmpty()) {
- binding.wcgDetailExploreInfoGenre.forEach {
- it.isSelected = false
- }
- }
- }
-
- detailExploreViewModel.selectedStatus.observe(viewLifecycleOwner) { selectedStatus ->
- val selectedChip = selectedStatus?.title
-
- listOf(
- binding.chipDetailExploreInfoStatusInSeries,
- binding.chipDetailExploreInfoStatusComplete,
- ).forEach { chip ->
- chip.isChecked = chip.text.toString() == selectedChip
- }
- }
-
- detailExploreViewModel.selectedRating.observe(viewLifecycleOwner) { selectedRating ->
- listOf(
- binding.chipDetailExploreInfoRatingLowest,
- binding.chipDetailExploreInfoRatingLower,
- binding.chipDetailExploreInfoRatingHigher,
- binding.chipDetailExploreInfoRatingHighest,
- ).forEach { chip ->
- val ratingValue =
- Rating.entries.find { chip.text.toString().contains(it.value.toString()) }
- chip.isChecked = selectedRating == ratingValue?.value
- }
- }
- }
-}
diff --git a/app/src/main/java/com/into/websoso/ui/detailExplore/info/model/Rating.kt b/app/src/main/java/com/into/websoso/ui/detailExplore/info/model/Rating.kt
deleted file mode 100644
index b31c640df..000000000
--- a/app/src/main/java/com/into/websoso/ui/detailExplore/info/model/Rating.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.into.websoso.ui.detailExplore.info.model
-
-enum class Rating(
- val value: Float,
-) {
- LOWEST(3.5f),
- LOWER(4.0f),
- HIGHER(4.5f),
- HIGHEST(4.8f),
-}
diff --git a/app/src/main/java/com/into/websoso/ui/detailExplore/keyword/DetailExploreClickListener.kt b/app/src/main/java/com/into/websoso/ui/detailExplore/keyword/DetailExploreClickListener.kt
deleted file mode 100644
index 2fe604822..000000000
--- a/app/src/main/java/com/into/websoso/ui/detailExplore/keyword/DetailExploreClickListener.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.into.websoso.ui.detailExplore.keyword
-
-interface DetailExploreClickListener {
- fun onNovelInquireButtonClick()
-
- fun onDetailSearchNovelButtonClick()
-
- fun onKeywordResetButtonClick()
-}
diff --git a/app/src/main/java/com/into/websoso/ui/detailExplore/keyword/DetailExploreKeywordFragment.kt b/app/src/main/java/com/into/websoso/ui/detailExplore/keyword/DetailExploreKeywordFragment.kt
deleted file mode 100644
index b91c62b0a..000000000
--- a/app/src/main/java/com/into/websoso/ui/detailExplore/keyword/DetailExploreKeywordFragment.kt
+++ /dev/null
@@ -1,322 +0,0 @@
-package com.into.websoso.ui.detailExplore.keyword
-
-import android.content.Intent
-import android.net.Uri
-import android.os.Bundle
-import android.view.View
-import androidx.activity.OnBackPressedCallback
-import androidx.core.view.children
-import androidx.core.view.forEach
-import androidx.core.view.isVisible
-import androidx.fragment.app.activityViewModels
-import com.into.websoso.R.color.bg_novel_rating_chip_background_selector
-import com.into.websoso.R.color.bg_novel_rating_chip_stroke_selector
-import com.into.websoso.R.color.bg_novel_rating_chip_text_selector
-import com.into.websoso.R.color.primary_100_6A5DFD
-import com.into.websoso.R.color.white
-import com.into.websoso.R.layout.fragment_detail_explore_keyword
-import com.into.websoso.R.style.body2
-import com.into.websoso.core.common.ui.base.BaseFragment
-import com.into.websoso.core.common.ui.custom.WebsosoChip
-import com.into.websoso.core.common.ui.model.CategoriesModel.CategoryModel
-import com.into.websoso.core.common.ui.model.CategoriesModel.CategoryModel.KeywordModel
-import com.into.websoso.core.common.ui.model.CategoriesModel.Companion.findKeywordByName
-import com.into.websoso.core.common.util.SingleEventHandler
-import com.into.websoso.core.common.util.toFloatPxFromDp
-import com.into.websoso.core.common.util.tracker.Tracker
-import com.into.websoso.core.resource.R.drawable.ic_novel_rating_keword_remove
-import com.into.websoso.core.resource.R.string.detail_explore_search_hint
-import com.into.websoso.core.resource.R.string.inquire_link
-import com.into.websoso.databinding.FragmentDetailExploreKeywordBinding
-import com.into.websoso.ui.detailExplore.DetailExploreViewModel
-import com.into.websoso.ui.detailExplore.keyword.adapter.DetailExploreKeywordAdapter
-import com.into.websoso.ui.detailExplore.keyword.model.DetailExploreKeywordUiState
-import com.into.websoso.ui.detailExploreResult.DetailExploreResultActivity
-import com.into.websoso.ui.detailExploreResult.model.DetailExploreFilteredModel
-import dagger.hilt.android.AndroidEntryPoint
-import javax.inject.Inject
-
-@AndroidEntryPoint
-class DetailExploreKeywordFragment : BaseFragment(fragment_detail_explore_keyword) {
- @Inject
- lateinit var tracker: Tracker
-
- private val detailExploreViewModel: DetailExploreViewModel by activityViewModels()
- private val singleEventHandler: SingleEventHandler by lazy { SingleEventHandler.from() }
- private val detailExploreKeywordAdapter: DetailExploreKeywordAdapter by lazy {
- DetailExploreKeywordAdapter(detailExploreViewModel::updateClickedChipState)
- }
-
- override fun onViewCreated(
- view: View,
- savedInstanceState: Bundle?,
- ) {
- super.onViewCreated(view, savedInstanceState)
-
- bindViewModel()
- setupAdapter()
- setupObserver()
- setupSearchKeyword()
- setupWebsosoSearchEditListener()
- setupBackButtonListener()
- }
-
- private fun bindViewModel() {
- binding.detailExploreViewModel = detailExploreViewModel
- binding.onClick = onDetailExploreKeywordButtonClick()
- binding.lifecycleOwner = viewLifecycleOwner
- }
-
- private fun onDetailExploreKeywordButtonClick() =
- object : DetailExploreClickListener {
- override fun onNovelInquireButtonClick() {
- tracker.trackEvent("contact_keyword")
- val inquireUrl = getString(inquire_link)
- val intent = Intent(Intent.ACTION_VIEW, Uri.parse(inquireUrl))
- startActivity(intent)
- }
-
- override fun onDetailSearchNovelButtonClick() {
- singleEventHandler.throttleFirst {
- navigateToDetailSearchResult()
- }
- }
-
- override fun onKeywordResetButtonClick() {
- detailExploreViewModel.updateSelectedKeywordValueClear()
- }
- }
-
- private fun navigateToDetailSearchResult() {
- val selectedGenres = detailExploreViewModel.selectedGenres.value ?: emptyList()
- val isCompleted = detailExploreViewModel.selectedStatus.value?.isCompleted
- val novelRating = detailExploreViewModel.selectedRating.value
-
- val keywordIds = detailExploreViewModel.uiState.value
- ?.categories
- ?.asSequence()
- ?.flatMap { it.keywords.asSequence() }
- ?.filter { it.isSelected }
- ?.map { it.keywordId }
- ?.toList() ?: emptyList()
-
- val intent = DetailExploreResultActivity.getIntent(
- context = requireContext(),
- DetailExploreFilteredModel(
- genres = selectedGenres,
- isCompleted = isCompleted,
- novelRating = novelRating,
- keywordIds = keywordIds,
- ),
- )
-
- startActivity(intent)
- }
-
- private fun setupAdapter() {
- binding.rvDetailExploreKeywordList.apply {
- adapter = detailExploreKeywordAdapter
- itemAnimator = null
- }
- }
-
- private fun setupObserver() {
- detailExploreViewModel.uiState.observe(viewLifecycleOwner) { uiState ->
- detailExploreKeywordAdapter.submitList(uiState.categories)
- setupSelectedScrollViewVisibility(uiState.categories)
- setupSelectedChips(uiState.categories)
- updateSearchKeywordResult(uiState)
- }
- }
-
- private fun setupSelectedScrollViewVisibility(categories: List) {
- val hasSelectedKeywords = categories.flatMap { it.keywords }.any { it.isSelected }
-
- binding.hsvRatingKeywordSelectedKeyword.isVisible = hasSelectedKeywords
- }
-
- private fun setupSelectedChips(categories: List) {
- val currentChipKeywords =
- binding.wcgDetailExploreKeywordSelectedKeyword.children
- .filterIsInstance()
- .map { it.text.toString() }
- .toList()
-
- val selectedKeywords =
- categories
- .asSequence()
- .flatMap { it.keywords.asSequence() }
- .filter { it.isSelected }
- .map { it.keywordName }
- .toList()
-
- val chipsToRemove = currentChipKeywords - selectedKeywords.toSet()
- val chipsToAdd = selectedKeywords - currentChipKeywords.toSet()
-
- chipsToRemove.forEach { keywordName ->
- removeSelectedChip(keywordName)
- }
-
- chipsToAdd.forEach { keywordName ->
- createSelectedChip(
- categories.findKeywordByName(keywordName)
- ?: throw IllegalArgumentException("Keyword not found: $keywordName"),
- )
- }
- }
-
- private fun removeSelectedChip(keywordName: String) {
- val chipToRemove = binding.wcgDetailExploreKeywordSelectedKeyword.children.find {
- (it as WebsosoChip).text == keywordName
- }
- chipToRemove?.let {
- binding.wcgDetailExploreKeywordSelectedKeyword.removeView(it)
- }
- }
-
- private fun createSelectedChip(selectedKeyword: KeywordModel) {
- WebsosoChip(requireContext())
- .apply {
- setWebsosoChipText(selectedKeyword.keywordName)
- setWebsosoChipTextAppearance(body2)
- setWebsosoChipTextColor(primary_100_6A5DFD)
- setWebsosoChipStrokeColor(primary_100_6A5DFD)
- setWebsosoChipBackgroundColor(white)
- setWebsosoChipPaddingVertical(12f.toFloatPxFromDp())
- setWebsosoChipPaddingHorizontal(6f.toFloatPxFromDp())
- setWebsosoChipRadius(20f.toFloatPxFromDp())
- setOnCloseIconClickListener {
- detailExploreViewModel.updateClickedChipState(
- selectedKeyword.keywordId,
- )
- }
- setWebsosoChipCloseIconVisibility(true)
- setWebsosoChipCloseIconDrawable(ic_novel_rating_keword_remove)
- setWebsosoChipCloseIconSize(10f.toFloatPxFromDp())
- setWebsosoChipCloseIconEndPadding(12f.toFloatPxFromDp())
- setCloseIconTintResource(primary_100_6A5DFD)
- }.also { websosoChip ->
- binding.wcgDetailExploreKeywordSelectedKeyword.addChip(websosoChip)
- }
- }
-
- private fun updateSearchKeywordResult(uiState: DetailExploreKeywordUiState) {
- val previousSearchResultKeywords =
- binding.wcgDetailExploreKeywordResult.children
- .toList()
- .map { it as WebsosoChip }
-
- when {
- !uiState.isSearchKeywordProceeding -> return
-
- uiState.isSearchResultKeywordsEmpty -> return
-
- uiState.searchResultKeywords.map { it.keywordName } == previousSearchResultKeywords.map { it.text.toString() } -> {
- updateSearchKeywordResultIsSelected(uiState)
- return
- }
-
- else -> {
- binding.wcgDetailExploreKeywordResult.removeAllViews()
- updateSearchKeywordResultWebsosoChips(uiState)
- }
- }
- }
-
- private fun updateSearchKeywordResultIsSelected(uiState: DetailExploreKeywordUiState) {
- binding.wcgDetailExploreKeywordResult.forEach { view ->
- val chip = view as? WebsosoChip ?: return@forEach
-
- val isSelected =
- uiState.categories
- .asSequence()
- .flatMap { it.keywords }
- .filter { it.isSelected }
- .any { it.keywordName == chip.text.toString() }
-
- chip.isSelected = isSelected
- }
- }
-
- private fun updateSearchKeywordResultWebsosoChips(uiState: DetailExploreKeywordUiState) {
- uiState.searchResultKeywords.forEach { keyword ->
- val isSelected = uiState.categories
- .asSequence()
- .flatMap { it.keywords }
- .any { it.keywordId == keyword.keywordId && it.isSelected }
-
- WebsosoChip(binding.root.context)
- .apply {
- setWebsosoChipText(keyword.keywordName)
- setWebsosoChipTextAppearance(body2)
- setWebsosoChipTextColor(bg_novel_rating_chip_text_selector)
- setWebsosoChipStrokeColor(bg_novel_rating_chip_stroke_selector)
- setWebsosoChipBackgroundColor(bg_novel_rating_chip_background_selector)
- setWebsosoChipPaddingVertical(12f.toFloatPxFromDp())
- setWebsosoChipPaddingHorizontal(6f.toFloatPxFromDp())
- setWebsosoChipRadius(20f.toFloatPxFromDp())
- setOnWebsosoChipClick {
- detailExploreViewModel.updateClickedChipState(keyword.keywordId)
- }
- this.isSelected = isSelected
- }.also { websosoChip ->
- binding.wcgDetailExploreKeywordResult.addChip(websosoChip)
- }
- }
- }
-
- private fun setupSearchKeyword() {
- binding.wsetDetailExploreKeywordSearch.apply {
- setWebsosoSearchHint(getString(detail_explore_search_hint))
- }
- }
-
- private fun setupWebsosoSearchEditListener() {
- binding.wsetDetailExploreKeywordSearch.setOnWebsosoSearchActionListener { _, _, _ ->
- performSearch()
- true
- }
-
- binding.wsetDetailExploreKeywordSearch.setOnWebsosoSearchFocusChangeListener { _, isFocused ->
- if (isFocused) detailExploreViewModel.updateIsSearchKeywordProceeding(true)
- }
-
- binding.wsetDetailExploreKeywordSearch.setOnWebsosoSearchClearClickListener {
- initSearchKeyword()
- }
- }
-
- private fun performSearch() {
- val input = binding.wsetDetailExploreKeywordSearch.getWebsosoSearchText()
- if (input.isEmpty()) {
- initSearchKeyword()
- return
- }
- detailExploreViewModel.updateKeyword(input)
- }
-
- private fun initSearchKeyword() {
- binding.wsetDetailExploreKeywordSearch.clearWebsosoSearchFocus()
- detailExploreViewModel.initSearchKeyword()
- }
-
- private fun setupBackButtonListener() {
- requireActivity().onBackPressedDispatcher.addCallback(
- viewLifecycleOwner,
- object : OnBackPressedCallback(true) {
- override fun handleOnBackPressed() {
- if (binding.wsetDetailExploreKeywordSearch.hasFocus()) {
- initSearchKeyword()
- } else {
- requireActivity().onBackPressedDispatcher.onBackPressed()
- }
- }
- },
- )
- }
-
- override fun onDestroyView() {
- initSearchKeyword()
- super.onDestroyView()
- }
-}
diff --git a/app/src/main/java/com/into/websoso/ui/detailExplore/keyword/adapter/DetailExploreKeywordAdapter.kt b/app/src/main/java/com/into/websoso/ui/detailExplore/keyword/adapter/DetailExploreKeywordAdapter.kt
deleted file mode 100644
index 750f35066..000000000
--- a/app/src/main/java/com/into/websoso/ui/detailExplore/keyword/adapter/DetailExploreKeywordAdapter.kt
+++ /dev/null
@@ -1,52 +0,0 @@
-package com.into.websoso.ui.detailExplore.keyword.adapter
-
-import android.view.LayoutInflater
-import android.view.ViewGroup
-import androidx.recyclerview.widget.DiffUtil
-import androidx.recyclerview.widget.ListAdapter
-import com.into.websoso.core.common.ui.model.CategoriesModel.CategoryModel
-import com.into.websoso.databinding.ItemCommonKeywordBinding
-
-class DetailExploreKeywordAdapter(
- private val onKeywordClick: (keywordId: Int) -> (Unit),
-) : ListAdapter(diffUtil) {
- override fun onCreateViewHolder(
- parent: ViewGroup,
- viewType: Int,
- ): DetailExploreKeywordViewHolder {
- val binding = ItemCommonKeywordBinding.inflate(
- LayoutInflater.from(parent.context),
- parent,
- false,
- )
- return DetailExploreKeywordViewHolder(binding, onKeywordClick)
- }
-
- override fun onBindViewHolder(
- holder: DetailExploreKeywordViewHolder,
- position: Int,
- ) {
- val item = getItem(position)
- holder.apply {
- when (isChipSetting) {
- true -> updateChipState(item)
- false -> initKeywordView(item)
- }
- }
- }
-
- companion object {
- private val diffUtil =
- object : DiffUtil.ItemCallback() {
- override fun areItemsTheSame(
- oldItem: CategoryModel,
- newItem: CategoryModel,
- ): Boolean = oldItem.categoryName == newItem.categoryName
-
- override fun areContentsTheSame(
- oldItem: CategoryModel,
- newItem: CategoryModel,
- ): Boolean = oldItem == newItem
- }
- }
-}
diff --git a/app/src/main/java/com/into/websoso/ui/detailExplore/keyword/adapter/DetailExploreKeywordViewHolder.kt b/app/src/main/java/com/into/websoso/ui/detailExplore/keyword/adapter/DetailExploreKeywordViewHolder.kt
deleted file mode 100644
index bb52b5dcb..000000000
--- a/app/src/main/java/com/into/websoso/ui/detailExplore/keyword/adapter/DetailExploreKeywordViewHolder.kt
+++ /dev/null
@@ -1,88 +0,0 @@
-package com.into.websoso.ui.detailExplore.keyword.adapter
-
-import androidx.constraintlayout.widget.ConstraintLayout
-import androidx.recyclerview.widget.RecyclerView
-import com.into.websoso.R
-import com.into.websoso.core.common.ui.custom.WebsosoChip
-import com.into.websoso.core.common.ui.model.CategoriesModel.CategoryModel
-import com.into.websoso.core.common.util.getS3ImageUrl
-import com.into.websoso.core.common.util.toFloatPxFromDp
-import com.into.websoso.core.common.util.toIntPxFromDp
-import com.into.websoso.databinding.ItemCommonKeywordBinding
-
-class DetailExploreKeywordViewHolder(
- private val binding: ItemCommonKeywordBinding,
- private val onKeywordClick: (keywordId: Int) -> Unit,
-) : RecyclerView.ViewHolder(binding.root) {
- var isChipSetting: Boolean = false
- private set
-
- init {
- binding.setupExpandToggleBtn()
- }
-
- private fun ItemCommonKeywordBinding.setupExpandToggleBtn() {
- ivNovelRatingKeywordToggle.setOnClickListener {
- ivNovelRatingKeywordToggle.isSelected = !ivNovelRatingKeywordToggle.isSelected
- val layoutParams =
- wcgNovelRatingKeyword.layoutParams as ConstraintLayout.LayoutParams
-
- when (ivNovelRatingKeywordToggle.isSelected) {
- true ->
- layoutParams.matchConstraintMaxHeight =
- ConstraintLayout.LayoutParams.WRAP_CONTENT
-
- false -> layoutParams.matchConstraintMaxHeight = 92.toIntPxFromDp()
- }
- wcgNovelRatingKeyword.layoutParams = layoutParams
- }
- }
-
- fun initKeywordView(category: CategoryModel) {
- val imageUrl: String = itemView.getS3ImageUrl(category.categoryImage)
-
- val updatedCategory = category.copy(
- categoryImage = imageUrl,
- )
-
- binding.apply {
- tvRatingKeyword.text = updatedCategory.categoryName
- categoryImageUrl = updatedCategory.categoryImage
- setupWebsosoChips(category)
- }
- isChipSetting = true
- }
-
- private fun ItemCommonKeywordBinding.setupWebsosoChips(category: CategoryModel) {
- wcgNovelRatingKeyword.removeAllViews()
- category.keywords.forEach { keyword ->
- WebsosoChip(binding.root.context)
- .apply {
- setWebsosoChipText(keyword.keywordName)
- setWebsosoChipTextAppearance(R.style.body2)
- setWebsosoChipTextColor(R.color.bg_novel_rating_chip_text_selector)
- setWebsosoChipStrokeColor(R.color.bg_novel_rating_chip_stroke_selector)
- setWebsosoChipBackgroundColor(R.color.bg_novel_rating_chip_background_selector)
- setWebsosoChipPaddingVertical(12f.toFloatPxFromDp())
- setWebsosoChipPaddingHorizontal(6f.toFloatPxFromDp())
- setWebsosoChipRadius(20f.toFloatPxFromDp())
- setOnWebsosoChipClick {
- onKeywordClick(keyword.keywordId)
- }
- isSelected = keyword.isSelected
- }.also { websosoChip -> wcgNovelRatingKeyword.addChip(websosoChip) }
- }
- }
-
- fun updateChipState(category: CategoryModel) {
- val keywordSelectionMap =
- category.keywords.associateBy({ it.keywordName }, { it.isSelected })
-
- (0 until binding.wcgNovelRatingKeyword.childCount)
- .map { binding.wcgNovelRatingKeyword.getChildAt(it) }
- .filterIsInstance()
- .forEach { chip ->
- chip.isSelected = keywordSelectionMap[chip.text] ?: false
- }
- }
-}
diff --git a/app/src/main/java/com/into/websoso/ui/detailExploreResult/DetailExploreResultActivity.kt b/app/src/main/java/com/into/websoso/ui/detailExploreResult/DetailExploreResultActivity.kt
index be6eeaf25..bf306b9d8 100644
--- a/app/src/main/java/com/into/websoso/ui/detailExploreResult/DetailExploreResultActivity.kt
+++ b/app/src/main/java/com/into/websoso/ui/detailExploreResult/DetailExploreResultActivity.kt
@@ -145,16 +145,7 @@ class DetailExploreResultActivity : BaseActivity(R.layout.dialog_detail_explore) {
- private val detailExploreResultInfoFragment: DetailExploreResultInfoFragment by lazy { DetailExploreResultInfoFragment() }
- private val detailExploreResultKeywordFragment: DetailExploreResultKeywordFragment by lazy { DetailExploreResultKeywordFragment() }
- private val detailExploreResultViewModel: DetailExploreResultViewModel by activityViewModels()
- private val singleEventHandler: SingleEventHandler by lazy { SingleEventHandler.from() }
-
- override fun onViewCreated(
- view: View,
- savedInstanceState: Bundle?,
- ) {
- super.onViewCreated(view, savedInstanceState)
-
- setupBottomSheet()
- initDetailExploreFragment()
- onReplaceFragmentButtonClick()
- onBottomSheetExitButtonClick()
- setupObserver()
- }
-
- private fun setupBottomSheet() {
- (dialog as BottomSheetDialog).apply {
- behavior.state = BottomSheetBehavior.STATE_EXPANDED
- behavior.isDraggable = false
- behavior.isHideable = false
- setCancelable(false)
- }
- }
-
- private fun initDetailExploreFragment() {
- childFragmentManager.commit {
- add(
- R.id.fcv_detail_explore,
- detailExploreResultInfoFragment,
- )
- }
- }
-
- private fun onReplaceFragmentButtonClick() {
- binding.tvDetailExploreInfoButton.setOnClickListener {
- singleEventHandler.throttleFirst {
- switchFragment(INFO)
- }
- }
-
- binding.tvDetailExploreKeywordButton.setOnClickListener {
- singleEventHandler.throttleFirst {
- switchFragment(KEYWORD)
- }
- }
- }
-
- private fun switchFragment(selectedFragmentTitle: SelectedFragmentTitle) {
- val fragmentToShow = when (selectedFragmentTitle) {
- INFO -> detailExploreResultInfoFragment
- KEYWORD -> {
- if (childFragmentManager.findFragmentById(R.id.fcv_detail_explore) !is DetailExploreResultKeywordFragment) {
- childFragmentManager.commit {
- add(R.id.fcv_detail_explore, detailExploreResultKeywordFragment)
- }
- }
- detailExploreResultKeywordFragment
- }
- }
-
- val fragmentToHide = when (selectedFragmentTitle) {
- INFO -> detailExploreResultKeywordFragment
- KEYWORD -> detailExploreResultInfoFragment
- }
-
- childFragmentManager.commit {
- setReorderingAllowed(true)
- show(fragmentToShow)
- hide(fragmentToHide)
- }
-
- updateButtonColors(selectedFragmentTitle)
- }
-
- private fun updateButtonColors(selectedFragmentTitle: SelectedFragmentTitle) {
- val selectedColor = requireContext().getColor(R.color.primary_100_6A5DFD)
- val defaultColor = requireContext().getColor(R.color.gray_200_949399)
-
- when (selectedFragmentTitle) {
- INFO -> {
- binding.apply {
- tvDetailExploreInfoButton.setTextColor(selectedColor)
- tvDetailExploreKeywordButton.setTextColor(defaultColor)
- viewDetailExploreSelectedInfoTab.isVisible = true
- viewDetailExploreSelectedKeywordTab.isVisible = false
- }
- }
-
- KEYWORD -> {
- binding.apply {
- tvDetailExploreKeywordButton.setTextColor(selectedColor)
- tvDetailExploreInfoButton.setTextColor(defaultColor)
- viewDetailExploreSelectedInfoTab.isVisible = false
- viewDetailExploreSelectedKeywordTab.isVisible = true
- }
- }
- }
- }
-
- private fun onBottomSheetExitButtonClick() {
- binding.ivDetailExploreExitButton.setOnClickListener {
- dismiss()
- }
- }
-
- private fun setupObserver() {
- detailExploreResultViewModel.isInfoChipSelected.observe(viewLifecycleOwner) { isVisible ->
- binding.ivDetailExploreInfoActiveDot.isVisible = isVisible
- }
-
- detailExploreResultViewModel.isKeywordChipSelected.observe(viewLifecycleOwner) { isVisible ->
- binding.ivDetailExploreKeywordActiveDot.isVisible = isVisible
- }
- }
-
- companion object {
- fun newInstance(): DetailExploreResultDialogBottomSheet = DetailExploreResultDialogBottomSheet()
- }
-}
diff --git a/app/src/main/java/com/into/websoso/ui/detailExploreResult/DetailExploreResultInfoFragment.kt b/app/src/main/java/com/into/websoso/ui/detailExploreResult/DetailExploreResultInfoFragment.kt
deleted file mode 100644
index 6d6b3ea33..000000000
--- a/app/src/main/java/com/into/websoso/ui/detailExploreResult/DetailExploreResultInfoFragment.kt
+++ /dev/null
@@ -1,173 +0,0 @@
-package com.into.websoso.ui.detailExploreResult
-
-import android.os.Bundle
-import android.view.View
-import androidx.fragment.app.activityViewModels
-import com.google.android.material.chip.Chip
-import com.into.websoso.R
-import com.into.websoso.core.common.ui.base.BaseFragment
-import com.into.websoso.core.common.ui.custom.WebsosoChip
-import com.into.websoso.core.common.util.SingleEventHandler
-import com.into.websoso.core.common.util.toFloatPxFromDp
-import com.into.websoso.databinding.FragmentDetailExploreResultInfoBinding
-import com.into.websoso.ui.detailExplore.info.model.Genre
-import com.into.websoso.ui.detailExplore.info.model.Rating
-import com.into.websoso.ui.detailExplore.info.model.SeriesStatus
-import dagger.hilt.android.AndroidEntryPoint
-
-@AndroidEntryPoint
-class DetailExploreResultInfoFragment :
- BaseFragment(R.layout.fragment_detail_explore_result_info) {
- private val detailExploreResultViewModel: DetailExploreResultViewModel by activityViewModels()
- private val singleEventHandler: SingleEventHandler by lazy { SingleEventHandler.from() }
-
- override fun onViewCreated(
- view: View,
- savedInstanceState: Bundle?,
- ) {
- super.onViewCreated(view, savedInstanceState)
-
- bindViewModel()
- onResetButtonClick()
- onDetailSearchNovelButtonClick()
- setupGenreChips()
- setupSeriesStatusChips()
- setupRatingChips()
- setupObserver()
- }
-
- private fun bindViewModel() {
- binding.detailExploreResultViewModel = detailExploreResultViewModel
- binding.lifecycleOwner = this
- }
-
- private fun onResetButtonClick() {
- binding.clDetailExploreInfoResetButton.setOnClickListener {
- detailExploreResultViewModel.updateSelectedInfoValueClear()
- }
- }
-
- private fun onDetailSearchNovelButtonClick() {
- binding.tvDetailExploreSearchButton.setOnClickListener {
- singleEventHandler.throttleFirst {
- detailExploreResultViewModel.updateSearchResult(isSearchButtonClick = true)
-
- (parentFragment as? DetailExploreResultDialogBottomSheet)?.dismiss()
-
- detailExploreResultViewModel.updateIsBottomSheetOpen(false)
- }
- }
- }
-
- private fun setupGenreChips() {
- val genres = Genre.entries
- genres.forEach { genre ->
- WebsosoChip(requireContext())
- .apply {
- setWebsosoChipText(genre.titleKr)
- setWebsosoChipTextAppearance(R.style.body2)
- setWebsosoChipTextColor(R.color.bg_detail_explore_chip_text_selector)
- setWebsosoChipStrokeColor(R.color.bg_detail_explore_chip_stroke_selector)
- setWebsosoChipBackgroundColor(R.color.bg_detail_explore_chip_background_selector)
- setWebsosoChipPaddingVertical(12f.toFloatPxFromDp())
- setWebsosoChipPaddingHorizontal(6f.toFloatPxFromDp())
- setWebsosoChipRadius(20f.toFloatPxFromDp())
- setOnWebsosoChipClick { detailExploreResultViewModel.updateSelectedGenres(genre) }
- }.also { websosoChip -> binding.wcgDetailExploreInfoGenre.addChip(websosoChip) }
- }
- }
-
- private fun setupSeriesStatusChips() {
- val seriesStatusChips = listOf(
- binding.chipDetailExploreInfoStatusInSeries,
- binding.chipDetailExploreInfoStatusComplete,
- )
-
- seriesStatusChips.forEach { chip ->
- setupChipCheckListener(chip) { isChecked ->
- when (isChecked) {
- true -> {
- seriesStatusChips.filter { it != chip }.forEach { it.isChecked = false }
- val status = SeriesStatus.from(chip.text.toString())
- detailExploreResultViewModel.updateSelectedSeriesStatus(status)
- }
-
- false -> {
- detailExploreResultViewModel.updateSelectedSeriesStatus(null)
- }
- }
- }
- }
- }
-
- private fun setupRatingChips() {
- val ratingChips = listOf(
- binding.chipDetailExploreInfoRatingLowest,
- binding.chipDetailExploreInfoRatingLower,
- binding.chipDetailExploreInfoRatingHigher,
- binding.chipDetailExploreInfoRatingHighest,
- )
-
- ratingChips.forEach { chip ->
- setupChipCheckListener(chip) { isChecked ->
- when (isChecked) {
- true -> {
- ratingChips.filter { it != chip }.forEach { it.isChecked = false }
- val ratingValue =
- Rating.entries.find {
- chip.text.toString().contains(it.value.toString())
- }
- detailExploreResultViewModel.updateSelectedRating(ratingValue?.value)
- }
-
- false -> detailExploreResultViewModel.updateSelectedRating(null)
- }
- }
- }
- }
-
- private fun setupChipCheckListener(
- chip: Chip,
- onCheckedChange: (Boolean) -> Unit,
- ) {
- chip.setOnCheckedChangeListener(null)
- chip.setOnCheckedChangeListener { _, isChecked -> onCheckedChange(isChecked) }
- }
-
- private fun setupObserver() {
- detailExploreResultViewModel.selectedGenres.observe(viewLifecycleOwner) { genres ->
- (0 until binding.wcgDetailExploreInfoGenre.childCount)
- .map { binding.wcgDetailExploreInfoGenre.getChildAt(it) }
- .filterIsInstance()
- .forEach { chip ->
- chip.isSelected = genres?.any { it.titleKr == chip.text } == true
- }
- }
-
- detailExploreResultViewModel.isNovelCompleted.observe(viewLifecycleOwner) { selectedStatus ->
- val selectedChip = selectedStatus?.let {
- SeriesStatus.fromIsCompleted(it).title
- }
-
- listOf(
- binding.chipDetailExploreInfoStatusInSeries,
- binding.chipDetailExploreInfoStatusComplete,
- ).forEach { chip ->
- chip.isChecked = chip.text.toString() == selectedChip
- }
- }
-
- detailExploreResultViewModel.selectedRating.observe(viewLifecycleOwner) { selectedRating ->
- listOf(
- binding.chipDetailExploreInfoRatingLowest,
- binding.chipDetailExploreInfoRatingLower,
- binding.chipDetailExploreInfoRatingHigher,
- binding.chipDetailExploreInfoRatingHighest,
- ).forEach { chip ->
- val ratingValue =
- Rating.entries.find { chip.text.toString().contains(it.value.toString()) }
- chip.isChecked = selectedRating == ratingValue?.value
- }
- }
- }
-}
diff --git a/app/src/main/java/com/into/websoso/ui/detailExploreResult/DetailExploreResultKeywordFragment.kt b/app/src/main/java/com/into/websoso/ui/detailExploreResult/DetailExploreResultKeywordFragment.kt
deleted file mode 100644
index 3399ff386..000000000
--- a/app/src/main/java/com/into/websoso/ui/detailExploreResult/DetailExploreResultKeywordFragment.kt
+++ /dev/null
@@ -1,299 +0,0 @@
-package com.into.websoso.ui.detailExploreResult
-
-import android.content.Intent
-import android.net.Uri
-import android.os.Bundle
-import android.view.View
-import androidx.activity.OnBackPressedCallback
-import androidx.core.view.children
-import androidx.core.view.forEach
-import androidx.core.view.isVisible
-import androidx.fragment.app.activityViewModels
-import com.into.websoso.R
-import com.into.websoso.R.color.bg_novel_rating_chip_background_selector
-import com.into.websoso.R.color.bg_novel_rating_chip_stroke_selector
-import com.into.websoso.R.color.bg_novel_rating_chip_text_selector
-import com.into.websoso.R.color.primary_100_6A5DFD
-import com.into.websoso.R.color.white
-import com.into.websoso.R.style.body2
-import com.into.websoso.core.common.ui.base.BaseFragment
-import com.into.websoso.core.common.ui.custom.WebsosoChip
-import com.into.websoso.core.common.ui.model.CategoriesModel.CategoryModel
-import com.into.websoso.core.common.ui.model.CategoriesModel.CategoryModel.KeywordModel
-import com.into.websoso.core.common.ui.model.CategoriesModel.Companion.findKeywordByName
-import com.into.websoso.core.common.util.SingleEventHandler
-import com.into.websoso.core.common.util.toFloatPxFromDp
-import com.into.websoso.core.common.util.tracker.Tracker
-import com.into.websoso.core.resource.R.drawable.ic_novel_rating_keword_remove
-import com.into.websoso.core.resource.R.string.detail_explore_search_hint
-import com.into.websoso.core.resource.R.string.inquire_link
-import com.into.websoso.databinding.FragmentDetailExploreResultKeywordBinding
-import com.into.websoso.ui.detailExplore.keyword.DetailExploreClickListener
-import com.into.websoso.ui.detailExplore.keyword.adapter.DetailExploreKeywordAdapter
-import com.into.websoso.ui.detailExploreResult.model.DetailExploreResultUiState
-import dagger.hilt.android.AndroidEntryPoint
-import javax.inject.Inject
-
-@AndroidEntryPoint
-class DetailExploreResultKeywordFragment :
- BaseFragment(R.layout.fragment_detail_explore_result_keyword) {
- @Inject
- lateinit var tracker: Tracker
-
- private val detailExploreResultViewModel: DetailExploreResultViewModel by activityViewModels()
- private val singleEventHandler: SingleEventHandler by lazy { SingleEventHandler.from() }
- private val detailExploreKeywordAdapter: DetailExploreKeywordAdapter by lazy {
- DetailExploreKeywordAdapter(detailExploreResultViewModel::updateClickedChipState)
- }
-
- override fun onViewCreated(
- view: View,
- savedInstanceState: Bundle?,
- ) {
- super.onViewCreated(view, savedInstanceState)
-
- bindViewModel()
- setupAdapter()
- setupObserver()
- setupSearchKeyword()
- setupWebsosoSearchEditListener()
- setupBackButtonListener()
- tracker.trackEvent("seek_result")
- }
-
- private fun bindViewModel() {
- binding.detailExploreResultViewModel = detailExploreResultViewModel
- binding.onClick = onDetailExploreKeywordButtonClick()
- binding.lifecycleOwner = viewLifecycleOwner
- }
-
- private fun onDetailExploreKeywordButtonClick() =
- object : DetailExploreClickListener {
- override fun onNovelInquireButtonClick() {
- val inquireUrl = getString(inquire_link)
- val intent = Intent(Intent.ACTION_VIEW, Uri.parse(inquireUrl))
- startActivity(intent)
- }
-
- override fun onDetailSearchNovelButtonClick() {
- singleEventHandler.throttleFirst {
- detailExploreResultViewModel.updateSearchResult(true)
-
- (parentFragment as? DetailExploreResultDialogBottomSheet)?.dismiss()
-
- detailExploreResultViewModel.updateIsBottomSheetOpen(false)
- }
- }
-
- override fun onKeywordResetButtonClick() {
- detailExploreResultViewModel.updateSelectedKeywordValueClear()
- }
- }
-
- private fun setupAdapter() {
- binding.rvDetailExploreKeywordList.apply {
- adapter = detailExploreKeywordAdapter
- itemAnimator = null
- }
- }
-
- private fun setupObserver() {
- detailExploreResultViewModel.uiState.observe(viewLifecycleOwner) { uiState ->
- detailExploreKeywordAdapter.submitList(uiState.categories)
- setupSelectedScrollViewVisibility(uiState.categories)
- setupSelectedChips(uiState.categories)
- updateSearchKeywordResult(uiState)
- }
- }
-
- private fun setupSelectedScrollViewVisibility(categories: List) {
- val hasSelectedKeywords = categories.flatMap { it.keywords }.any { it.isSelected }
-
- binding.hsvRatingKeywordSelectedKeyword.isVisible = hasSelectedKeywords
- }
-
- private fun setupSelectedChips(categories: List) {
- val currentChipKeywords =
- binding.wcgDetailExploreKeywordSelectedKeyword.children
- .filterIsInstance()
- .map { it.text.toString() }
- .toList()
-
- val selectedKeywords =
- categories
- .asSequence()
- .flatMap { it.keywords.asSequence() }
- .filter { it.isSelected }
- .map { it.keywordName }
- .toList()
-
- val chipsToRemove = currentChipKeywords - selectedKeywords.toSet()
- val chipsToAdd = selectedKeywords - currentChipKeywords.toSet()
-
- chipsToRemove.forEach { keywordName ->
- removeSelectedChip(keywordName)
- }
-
- chipsToAdd.forEach { keywordName ->
- createSelectedChip(
- categories.findKeywordByName(keywordName)
- ?: throw IllegalArgumentException("Keyword not found: $keywordName"),
- )
- }
- }
-
- private fun removeSelectedChip(keywordName: String) {
- val chipToRemove = binding.wcgDetailExploreKeywordSelectedKeyword.children.find {
- (it as WebsosoChip).text == keywordName
- }
- chipToRemove?.let {
- binding.wcgDetailExploreKeywordSelectedKeyword.removeView(it)
- }
- }
-
- private fun createSelectedChip(selectedKeyword: KeywordModel) {
- WebsosoChip(requireContext())
- .apply {
- setWebsosoChipText(selectedKeyword.keywordName)
- setWebsosoChipTextAppearance(body2)
- setWebsosoChipTextColor(primary_100_6A5DFD)
- setWebsosoChipStrokeColor(primary_100_6A5DFD)
- setWebsosoChipBackgroundColor(white)
- setWebsosoChipPaddingVertical(12f.toFloatPxFromDp())
- setWebsosoChipPaddingHorizontal(4f.toFloatPxFromDp())
- setWebsosoChipRadius(20f.toFloatPxFromDp())
- setOnCloseIconClickListener {
- detailExploreResultViewModel.updateClickedChipState(
- selectedKeyword.keywordId,
- )
- }
- setWebsosoChipCloseIconVisibility(true)
- setWebsosoChipCloseIconDrawable(ic_novel_rating_keword_remove)
- setWebsosoChipCloseIconSize(20f)
- setWebsosoChipCloseIconEndPadding(18f)
- setCloseIconTintResource(primary_100_6A5DFD)
- }.also { websosoChip ->
- binding.wcgDetailExploreKeywordSelectedKeyword.addChip(websosoChip)
- }
- }
-
- private fun updateSearchKeywordResult(uiState: DetailExploreResultUiState) {
- val previousSearchResultKeywords =
- binding.wcgDetailExploreKeywordResult.children
- .toList()
- .map { it as WebsosoChip }
-
- when {
- !uiState.isSearchKeywordProceeding -> return
-
- uiState.isSearchResultKeywordsEmpty -> return
-
- uiState.searchResultKeywords.map { it.keywordName } == previousSearchResultKeywords.map { it.text.toString() } -> {
- updateSearchKeywordResultIsSelected(uiState)
- return
- }
-
- else -> {
- binding.wcgDetailExploreKeywordResult.removeAllViews()
- updateSearchKeywordResultWebsosoChips(uiState)
- }
- }
- }
-
- private fun updateSearchKeywordResultIsSelected(uiState: DetailExploreResultUiState) {
- binding.wcgDetailExploreKeywordResult.forEach { view ->
- val chip = view as? WebsosoChip ?: return@forEach
-
- val isSelected =
- uiState.categories
- .asSequence()
- .flatMap { it.keywords }
- .filter { it.isSelected }
- .any { it.keywordName == chip.text.toString() }
-
- chip.isSelected = isSelected
- }
- }
-
- private fun updateSearchKeywordResultWebsosoChips(uiState: DetailExploreResultUiState) {
- uiState.searchResultKeywords.forEach { keyword ->
- val isSelected = uiState.categories
- .asSequence()
- .flatMap { it.keywords }
- .any { it.keywordId == keyword.keywordId && it.isSelected }
-
- WebsosoChip(binding.root.context)
- .apply {
- setWebsosoChipText(keyword.keywordName)
- setWebsosoChipTextAppearance(body2)
- setWebsosoChipTextColor(bg_novel_rating_chip_text_selector)
- setWebsosoChipStrokeColor(bg_novel_rating_chip_stroke_selector)
- setWebsosoChipBackgroundColor(bg_novel_rating_chip_background_selector)
- setWebsosoChipPaddingVertical(12f.toFloatPxFromDp())
- setWebsosoChipPaddingHorizontal(6f.toFloatPxFromDp())
- setWebsosoChipRadius(20f.toFloatPxFromDp())
- setOnWebsosoChipClick {
- detailExploreResultViewModel.updateClickedChipState(keyword.keywordId)
- }
- this.isSelected = isSelected
- }.also { websosoChip ->
- binding.wcgDetailExploreKeywordResult.addChip(websosoChip)
- }
- }
- }
-
- private fun setupSearchKeyword() {
- binding.wsetDetailExploreKeywordSearch.apply {
- setWebsosoSearchHint(getString(detail_explore_search_hint))
- }
- }
-
- private fun setupWebsosoSearchEditListener() {
- binding.wsetDetailExploreKeywordSearch.setOnWebsosoSearchActionListener { _, _, _ ->
- performSearch()
- true
- }
-
- binding.wsetDetailExploreKeywordSearch.setOnWebsosoSearchFocusChangeListener { _, isFocused ->
- if (isFocused) detailExploreResultViewModel.updateIsSearchKeywordProceeding(true)
- }
-
- binding.wsetDetailExploreKeywordSearch.setOnWebsosoSearchClearClickListener {
- initSearchKeyword()
- }
- }
-
- private fun performSearch() {
- val input = binding.wsetDetailExploreKeywordSearch.getWebsosoSearchText()
- if (input.isEmpty()) {
- initSearchKeyword()
- return
- }
- detailExploreResultViewModel.updateKeyword(input)
- }
-
- private fun initSearchKeyword() {
- binding.wsetDetailExploreKeywordSearch.clearWebsosoSearchFocus()
- detailExploreResultViewModel.initSearchKeyword()
- }
-
- private fun setupBackButtonListener() {
- requireActivity().onBackPressedDispatcher.addCallback(
- viewLifecycleOwner,
- object : OnBackPressedCallback(true) {
- override fun handleOnBackPressed() {
- if (binding.wsetDetailExploreKeywordSearch.hasFocus()) {
- initSearchKeyword()
- } else {
- requireActivity().onBackPressedDispatcher.onBackPressed()
- }
- }
- },
- )
- }
-
- override fun onDestroyView() {
- initSearchKeyword()
- super.onDestroyView()
- }
-}
diff --git a/app/src/main/java/com/into/websoso/ui/detailExploreResult/DetailExploreResultViewModel.kt b/app/src/main/java/com/into/websoso/ui/detailExploreResult/DetailExploreResultViewModel.kt
index d25edb937..f8ae543a9 100644
--- a/app/src/main/java/com/into/websoso/ui/detailExploreResult/DetailExploreResultViewModel.kt
+++ b/app/src/main/java/com/into/websoso/ui/detailExploreResult/DetailExploreResultViewModel.kt
@@ -4,13 +4,9 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
-import androidx.lifecycle.map
import androidx.lifecycle.viewModelScope
-import com.into.websoso.core.common.ui.model.CategoriesModel
-import com.into.websoso.data.repository.KeywordRepository
import com.into.websoso.domain.usecase.GetDetailExploreResultUseCase
import com.into.websoso.ui.detailExplore.info.model.Genre
-import com.into.websoso.ui.detailExplore.info.model.SeriesStatus
import com.into.websoso.ui.detailExploreResult.model.DetailExploreFilteredModel
import com.into.websoso.ui.detailExploreResult.model.DetailExploreResultUiState
import com.into.websoso.ui.mapper.toUi
@@ -23,127 +19,49 @@ class DetailExploreResultViewModel
@Inject
constructor(
private val getDetailExploreResultUseCase: GetDetailExploreResultUseCase,
- private val keywordRepository: KeywordRepository,
) : ViewModel() {
private val _uiState: MutableLiveData =
MutableLiveData(DetailExploreResultUiState())
val uiState: LiveData get() = _uiState
- private val _selectedGenres: MutableLiveData?> =
- MutableLiveData(mutableListOf())
- val selectedGenres: LiveData?> get() = _selectedGenres.map { it?.toList() }
-
- private val _isNovelCompleted: MutableLiveData = MutableLiveData()
- val isNovelCompleted: LiveData get() = _isNovelCompleted
-
- private val _selectedSeriesStatus: MutableLiveData = MutableLiveData()
-
- private val _selectedRating: MutableLiveData = MutableLiveData()
- val selectedRating: LiveData get() = _selectedRating
-
- val ratings: List = listOf(3.5f, 4.0f, 4.5f, 4.8f)
-
- private val _selectedKeywordIds: MutableLiveData?> =
- MutableLiveData(mutableListOf())
- val selectedKeywordIds: LiveData?> get() = _selectedKeywordIds.map { it?.toList() }
+ private val filterGenres: MutableLiveData?> = MutableLiveData(emptyList())
+ private val filterIsNovelCompleted: MutableLiveData = MutableLiveData()
+ private val filterRating: MutableLiveData = MutableLiveData()
+ private val filterKeywordIds: MutableLiveData?> = MutableLiveData(emptyList())
private val _appliedFiltersMessage: MediatorLiveData = MediatorLiveData()
val appliedFiltersMessage: LiveData get() = _appliedFiltersMessage
- private val _isInfoChipSelected: MediatorLiveData = MediatorLiveData(false)
- val isInfoChipSelected: LiveData get() = _isInfoChipSelected
-
- private val _isKeywordChipSelected: MediatorLiveData = MediatorLiveData(false)
- val isKeywordChipSelected: LiveData get() = _isKeywordChipSelected
-
private val _isNovelResultEmptyBoxVisibility: MutableLiveData = MutableLiveData(false)
val isNovelResultEmptyBoxVisibility: LiveData get() = _isNovelResultEmptyBoxVisibility
- private val isBottomSheetOpen = MutableLiveData(false)
-
init {
_appliedFiltersMessage.apply {
- addSource(_selectedGenres) { updateMessage() }
- addSource(_isNovelCompleted) { updateMessage() }
- addSource(_selectedRating) { updateMessage() }
- addSource(_selectedKeywordIds) { updateMessage() }
- }
-
- _isInfoChipSelected.apply {
- addSource(_selectedGenres) { isInfoChipSelectedEnabled() }
- addSource(_isNovelCompleted) { isInfoChipSelectedEnabled() }
- addSource(_selectedRating) { isInfoChipSelectedEnabled() }
+ addSource(filterGenres) { updateMessage() }
+ addSource(filterIsNovelCompleted) { updateMessage() }
+ addSource(filterRating) { updateMessage() }
+ addSource(filterKeywordIds) { updateMessage() }
}
-
- _isKeywordChipSelected.addSource(_selectedKeywordIds) { isKeywordChipSelectedEnabled() }
- updateKeyword(null)
}
private fun updateMessage() {
- if (isBottomSheetOpen.value == true) return
-
val appliedFilters = mutableListOf()
- _selectedGenres.value?.let { genres ->
- if (genres.isNotEmpty()) {
- appliedFilters.add(GENRES_LABEL)
- }
- }
-
- _isNovelCompleted.value?.let {
- appliedFilters.add(NOVEL_COMPLETED_LABEL)
- }
-
- _selectedRating.value?.let {
- appliedFilters.add(RATING_LABEL)
- }
-
- _selectedKeywordIds.value?.let { keywords ->
- if (keywords.isNotEmpty()) {
- appliedFilters.add(KEYWORDS_LABEL)
- }
- }
+ if (filterGenres.value?.isNotEmpty() == true) appliedFilters.add(GENRES_LABEL)
+ if (filterIsNovelCompleted.value != null) appliedFilters.add(NOVEL_COMPLETED_LABEL)
+ if (filterRating.value != null) appliedFilters.add(RATING_LABEL)
+ if (filterKeywordIds.value?.isNotEmpty() == true) appliedFilters.add(KEYWORDS_LABEL)
_appliedFiltersMessage.value = appliedFilters.joinToString(FILTER_SEPARATOR)
}
- private fun isInfoChipSelectedEnabled() {
- val isGenreChipSelected: Boolean = _selectedGenres.value?.isNotEmpty() == true
- val isStatusChipSelected: Boolean = _isNovelCompleted.value != null
- val isRatingChipSelected: Boolean = _selectedRating.value != null
-
- _isInfoChipSelected.value =
- isGenreChipSelected ||
- isStatusChipSelected ||
- isRatingChipSelected
- }
-
- private fun isKeywordChipSelectedEnabled() {
- _isKeywordChipSelected.value = selectedKeywordIds.value?.isNotEmpty()
- }
-
fun updatePreviousSearchFilteredValue(detailExploreFilteredModel: DetailExploreFilteredModel) {
- _selectedGenres.value = detailExploreFilteredModel.genres?.toMutableList()
- _isNovelCompleted.value = detailExploreFilteredModel.isCompleted
- _selectedRating.value = detailExploreFilteredModel.novelRating
- _selectedKeywordIds.value = detailExploreFilteredModel.keywordIds?.toMutableList()
-
- val currentUiState = _uiState.value ?: return
- val selectedKeywordIds = _selectedKeywordIds.value.orEmpty()
-
- val updatedCategories = currentUiState.categories.map { category ->
- val updatedKeywords = category.keywords.map { existingKeyword ->
- if (selectedKeywordIds.contains(existingKeyword.keywordId)) {
- existingKeyword.copy(isSelected = !existingKeyword.isSelected)
- } else {
- existingKeyword
- }
- }
- category.copy(keywords = updatedKeywords)
- }
+ filterGenres.value = detailExploreFilteredModel.genres
+ filterIsNovelCompleted.value = detailExploreFilteredModel.isCompleted
+ filterRating.value = detailExploreFilteredModel.novelRating
+ filterKeywordIds.value = detailExploreFilteredModel.keywordIds
updateSearchResult(true)
- _uiState.value = currentUiState.copy(categories = updatedCategories)
}
fun updateSearchResult(isSearchButtonClick: Boolean) {
@@ -152,10 +70,10 @@ class DetailExploreResultViewModel
viewModelScope.launch {
runCatching {
getDetailExploreResultUseCase(
- genres = selectedGenres.value?.map { it.titleEn },
- isCompleted = isNovelCompleted.value,
- novelRating = selectedRating.value,
- keywordIds = selectedKeywordIds.value,
+ genres = filterGenres.value?.map { it.titleEn },
+ isCompleted = filterIsNovelCompleted.value,
+ novelRating = filterRating.value,
+ keywordIds = filterKeywordIds.value,
isSearchButtonClick = isSearchButtonClick,
)
}.onSuccess { results ->
@@ -190,153 +108,6 @@ class DetailExploreResultViewModel
}
}
- fun updateSelectedInfoValueClear() {
- _selectedGenres.value = mutableListOf()
- _isNovelCompleted.value = null
- _selectedRating.value = null
- }
-
- fun updateSelectedGenres(genre: Genre) {
- val currentGenres = _selectedGenres.value?.toMutableList() ?: mutableListOf()
-
- _selectedGenres.value = when (currentGenres.contains(genre)) {
- true -> {
- currentGenres.remove(genre)
- currentGenres
- }
-
- false -> {
- currentGenres.add(genre)
- currentGenres
- }
- }
- }
-
- fun updateSelectedSeriesStatus(status: SeriesStatus?) {
- _selectedSeriesStatus.value = status
- _isNovelCompleted.value = status?.isCompleted
- }
-
- fun updateSelectedRating(rating: Float?) {
- _selectedRating.value = rating
- }
-
- fun updateKeyword(searchWord: String?) {
- viewModelScope.launch {
- runCatching {
- keywordRepository.fetchKeywords(searchWord)
- }.onSuccess { keywordsList ->
- val categoriesModel =
- CategoriesModel(categories = keywordsList.categories.map { it.toUi() })
-
- val selectedKeywordIds = selectedKeywordIds.value.orEmpty()
-
- val updatedCategories = categoriesModel.categories.map { category ->
- val updatedKeywords = category.keywords.map { keyword ->
- if (selectedKeywordIds.contains(keyword.keywordId)) {
- keyword.copy(isSelected = true)
- } else {
- keyword
- }
- }
- category.copy(keywords = updatedKeywords)
- }
-
- _uiState.value = when (searchWord) {
- null -> {
- uiState.value?.copy(
- loading = false,
- categories = updatedCategories,
- )
- }
-
- else -> {
- val results = updatedCategories.flatMap { it.keywords }
- uiState.value?.copy(
- loading = false,
- searchResultKeywords = results,
- isInitialSearchKeyword = false,
- isSearchResultKeywordsEmpty = results.isEmpty(),
- )
- }
- }
- }.onFailure {
- _uiState.value = uiState.value?.copy(
- loading = false,
- error = true,
- )
- }
- }
- }
-
- fun updateClickedChipState(keywordId: Int) {
- val currentUiState = _uiState.value ?: return
-
- val updatedCategories = currentUiState.categories.map { category ->
- val updatedKeywords = category.keywords.map { existingKeyword ->
- when (existingKeyword.keywordId == keywordId) {
- true -> existingKeyword.copy(isSelected = !existingKeyword.isSelected)
- false -> existingKeyword
- }
- }
- category.copy(keywords = updatedKeywords)
- }
-
- val selectedKeywordIds = updatedCategories.flatMap { category ->
- category.keywords.filter { it.isSelected }.map { it.keywordId }
- }
-
- val isAnyKeywordSelected = updatedCategories.any { category ->
- category.keywords.any { it.isSelected }
- }
-
- _selectedKeywordIds.value = selectedKeywordIds.toMutableList()
- _isKeywordChipSelected.value = isAnyKeywordSelected
- _uiState.value = currentUiState.copy(categories = updatedCategories)
- }
-
- fun updateSelectedKeywordValueClear() {
- val currentState = _uiState.value ?: return
- val updatedCategories = currentState.categories
-
- val resetCategories = updatedCategories.map { category ->
- category.copy(
- keywords = category.keywords.map { keyword ->
- keyword.copy(isSelected = false)
- },
- )
- }
-
- _selectedKeywordIds.value = mutableListOf()
- _uiState.value = currentState.copy(categories = resetCategories)
- _isKeywordChipSelected.value = false
- }
-
- fun updateIsSearchKeywordProceeding(isProceeding: Boolean) {
- uiState.value?.let { uiState ->
- _uiState.value = uiState.copy(
- isSearchKeywordProceeding = isProceeding,
- )
- }
- }
-
- fun initSearchKeyword() {
- uiState.value?.let { uiState ->
- _uiState.value = uiState.copy(
- searchResultKeywords = emptyList(),
- isSearchKeywordProceeding = false,
- isInitialSearchKeyword = true,
- isSearchResultKeywordsEmpty = false,
- )
- }
- }
-
- fun updateIsBottomSheetOpen(isBottomSheetOpen: Boolean) {
- this.isBottomSheetOpen.value = isBottomSheetOpen
-
- if (!isBottomSheetOpen) updateMessage()
- }
-
companion object {
private const val GENRES_LABEL = "장르"
private const val NOVEL_COMPLETED_LABEL = "연재상태"
diff --git a/app/src/main/java/com/into/websoso/ui/detailExploreResult/model/DetailExploreResultUiState.kt b/app/src/main/java/com/into/websoso/ui/detailExploreResult/model/DetailExploreResultUiState.kt
index 0be8226de..a5cba5f8d 100644
--- a/app/src/main/java/com/into/websoso/ui/detailExploreResult/model/DetailExploreResultUiState.kt
+++ b/app/src/main/java/com/into/websoso/ui/detailExploreResult/model/DetailExploreResultUiState.kt
@@ -1,6 +1,5 @@
package com.into.websoso.ui.detailExploreResult.model
-import com.into.websoso.core.common.ui.model.CategoriesModel.CategoryModel
import com.into.websoso.ui.normalExplore.model.NormalExploreModel
data class DetailExploreResultUiState(
@@ -9,9 +8,4 @@ data class DetailExploreResultUiState(
val isLoadable: Boolean = false,
val novels: List = emptyList(),
val novelCount: Long = 0,
- val categories: List = emptyList(),
- val searchResultKeywords: List = emptyList(),
- val isSearchKeywordProceeding: Boolean = false,
- val isInitialSearchKeyword: Boolean = true,
- val isSearchResultKeywordsEmpty: Boolean = false,
)
diff --git a/app/src/main/java/com/into/websoso/ui/main/MainActivity.kt b/app/src/main/java/com/into/websoso/ui/main/MainActivity.kt
index 36ce24f74..2c6fad011 100644
--- a/app/src/main/java/com/into/websoso/ui/main/MainActivity.kt
+++ b/app/src/main/java/com/into/websoso/ui/main/MainActivity.kt
@@ -12,7 +12,6 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.commit
import com.google.firebase.messaging.FirebaseMessaging
import com.into.websoso.R.id.fcv_main
-import com.into.websoso.R.id.menu_explore
import com.into.websoso.R.id.menu_feed
import com.into.websoso.R.id.menu_home
import com.into.websoso.R.id.menu_library
@@ -24,12 +23,10 @@ import com.into.websoso.core.resource.R.drawable.ic_blocked_user_snack_bar
import com.into.websoso.core.resource.R.string.main_back_press
import com.into.websoso.databinding.ActivityMainBinding
import com.into.websoso.ui.common.dialog.LoginRequestDialogFragment
-import com.into.websoso.ui.main.MainActivity.FragmentType.EXPLORE
import com.into.websoso.ui.main.MainActivity.FragmentType.FEED
import com.into.websoso.ui.main.MainActivity.FragmentType.HOME
import com.into.websoso.ui.main.MainActivity.FragmentType.LIBRARY
import com.into.websoso.ui.main.MainActivity.FragmentType.MY_PAGE
-import com.into.websoso.ui.main.explore.ExploreFragment
import com.into.websoso.ui.main.feed.FeedFragment
import com.into.websoso.ui.main.home.HomeFragment
import com.into.websoso.ui.main.library.LibraryFragment
@@ -44,7 +41,6 @@ class MainActivity : BaseActivity(activity_main) {
private val fragmentTags = mapOf(
menu_home to HomeFragment.TAG,
- menu_explore to ExploreFragment.TAG,
menu_feed to FeedFragment.TAG,
menu_library to LibraryFragment.TAG,
menu_my_page to MyPageFragment.TAG,
@@ -168,7 +164,6 @@ class MainActivity : BaseActivity(activity_main) {
private fun findOrCreateFragment(tag: String): Fragment =
supportFragmentManager.findFragmentByTag(tag) ?: when (tag) {
HomeFragment.TAG -> HomeFragment()
- ExploreFragment.TAG -> ExploreFragment()
FeedFragment.TAG -> FeedFragment()
LibraryFragment.TAG -> LibraryFragment()
MyPageFragment.TAG -> MyPageFragment()
@@ -203,7 +198,6 @@ class MainActivity : BaseActivity(activity_main) {
private fun handleNavigation(destination: FragmentType?) {
val menuId = when (destination) {
- EXPLORE -> menu_explore
MY_PAGE -> menu_my_page
FEED -> menu_feed
LIBRARY -> menu_library
@@ -257,7 +251,6 @@ class MainActivity : BaseActivity(activity_main) {
) {
LIBRARY(menu_library),
HOME(menu_home),
- EXPLORE(menu_explore),
FEED(menu_feed),
MY_PAGE(menu_my_page),
;
diff --git a/app/src/main/java/com/into/websoso/ui/main/explore/ExploreFragment.kt b/app/src/main/java/com/into/websoso/ui/main/explore/ExploreFragment.kt
deleted file mode 100644
index 83359a1c4..000000000
--- a/app/src/main/java/com/into/websoso/ui/main/explore/ExploreFragment.kt
+++ /dev/null
@@ -1,100 +0,0 @@
-package com.into.websoso.ui.main.explore
-
-import android.os.Bundle
-import android.view.View
-import androidx.fragment.app.viewModels
-import com.into.websoso.R
-import com.into.websoso.core.common.ui.base.BaseFragment
-import com.into.websoso.core.common.util.SingleEventHandler
-import com.into.websoso.core.common.util.tracker.Tracker
-import com.into.websoso.databinding.FragmentExploreBinding
-import com.into.websoso.ui.detailExplore.DetailExploreDialogBottomSheet
-import com.into.websoso.ui.main.explore.adapter.SosoPickAdapter
-import com.into.websoso.ui.normalExplore.NormalExploreActivity
-import com.into.websoso.ui.novelDetail.NovelDetailActivity
-import dagger.hilt.android.AndroidEntryPoint
-import javax.inject.Inject
-
-@AndroidEntryPoint
-class ExploreFragment : BaseFragment(R.layout.fragment_explore) {
- @Inject
- lateinit var tracker: Tracker
-
- private val sosoPickAdapter: SosoPickAdapter by lazy { SosoPickAdapter(::navigateToNovelDetail) }
- private val singleEventHandler: SingleEventHandler by lazy { SingleEventHandler.from() }
- private val exploreViewModel: ExploreViewModel by viewModels()
-
- override fun onViewCreated(
- view: View,
- savedInstanceState: Bundle?,
- ) {
- super.onViewCreated(view, savedInstanceState)
-
- initSosoPickAdapter()
- onNormalSearchButtonClick()
- onDetailExploreButtonClick()
- onReloadPageButtonClick()
- setupObserver()
- tracker.trackEvent("search")
- }
-
- private fun initSosoPickAdapter() {
- binding.rvExploreSosoPick.adapter = sosoPickAdapter
- binding.rvExploreSosoPick.setHasFixedSize(true)
- }
-
- private fun navigateToNovelDetail(novelId: Long) {
- tracker.trackEvent(
- eventName = "soso_pick",
- properties = mapOf("novelId" to novelId),
- )
- val intent = NovelDetailActivity.getIntent(requireContext(), novelId)
- startActivity(intent)
- }
-
- private fun onNormalSearchButtonClick() {
- binding.clExploreNormalSearch.setOnClickListener {
- tracker.trackEvent("general_search")
- val intent = NormalExploreActivity.getIntent(requireContext())
- startActivity(intent)
- }
- }
-
- private fun onDetailExploreButtonClick() {
- binding.clExploreDetailSearch.setOnClickListener {
- singleEventHandler.throttleFirst {
- showDetailExploreDialog()
- }
- }
- }
-
- private fun showDetailExploreDialog() {
- val detailExploreBottomSheet = DetailExploreDialogBottomSheet.newInstance()
- detailExploreBottomSheet.show(childFragmentManager, DETAIL_BOTTOM_SHEET_TAG)
- }
-
- private fun onReloadPageButtonClick() {
- binding.wllExplore.setReloadButtonClickListener {
- binding.wllExplore.setErrorLayoutVisibility(false)
- exploreViewModel.updateSosoPicks()
- }
- }
-
- private fun setupObserver() {
- exploreViewModel.uiState.observe(viewLifecycleOwner) { uiState ->
- when {
- uiState.loading -> binding.wllExplore.setWebsosoLoadingVisibility(true)
- uiState.error -> binding.wllExplore.setLoadingLayoutVisibility(false)
- !uiState.loading -> {
- binding.wllExplore.setWebsosoLoadingVisibility(false)
- sosoPickAdapter.submitList(uiState.sosoPicks)
- }
- }
- }
- }
-
- companion object {
- private const val DETAIL_BOTTOM_SHEET_TAG = "DetailExploreDialogBottomSheet"
- const val TAG = "ExploreFragment"
- }
-}
diff --git a/app/src/main/java/com/into/websoso/ui/main/home/HomeFragment.kt b/app/src/main/java/com/into/websoso/ui/main/home/HomeFragment.kt
index e400af729..84c069b84 100644
--- a/app/src/main/java/com/into/websoso/ui/main/home/HomeFragment.kt
+++ b/app/src/main/java/com/into/websoso/ui/main/home/HomeFragment.kt
@@ -19,16 +19,16 @@ import com.into.websoso.core.common.ui.model.ResultFrom.NormalExploreBack
import com.into.websoso.core.common.ui.model.ResultFrom.Notification
import com.into.websoso.core.common.ui.model.ResultFrom.NovelDetailBack
import com.into.websoso.core.common.ui.model.ResultFrom.ProfileEditSuccess
+import com.into.websoso.core.common.util.SingleEventHandler
import com.into.websoso.core.common.util.collectWithLifecycle
import com.into.websoso.core.common.util.tracker.Tracker
-import com.into.websoso.core.resource.R.string.home_nickname_interest_feed
import com.into.websoso.databinding.FragmentHomeBinding
+import com.into.websoso.ui.detailExplore.DetailExploreActivity
import com.into.websoso.ui.feedDetail.FeedDetailActivity
import com.into.websoso.ui.main.MainViewModel
import com.into.websoso.ui.main.home.adpater.PopularFeedsAdapter
import com.into.websoso.ui.main.home.adpater.PopularNovelsAdapter
import com.into.websoso.ui.main.home.adpater.RecommendedNovelsByUserTasteAdapter
-import com.into.websoso.ui.main.home.adpater.UserInterestFeedAdapter
import com.into.websoso.ui.main.home.dialog.TermsAgreementDialogFragment
import com.into.websoso.ui.normalExplore.NormalExploreActivity
import com.into.websoso.ui.notification.NotificationActivity
@@ -45,6 +45,8 @@ class HomeFragment : BaseFragment(fragment_home) {
private val homeViewModel: HomeViewModel by viewModels()
private val mainViewModel: MainViewModel by activityViewModels()
+ private val singleEventHandler: SingleEventHandler by lazy { SingleEventHandler.from() }
+
private val homeResultLauncher: ActivityResultLauncher =
registerForActivityResult(StartActivityForResult()) { result ->
when (result.resultCode) {
@@ -62,10 +64,6 @@ class HomeFragment : BaseFragment(fragment_home) {
PopularFeedsAdapter(::onPopularFeedClick)
}
- private val userInterestFeedAdapter: UserInterestFeedAdapter by lazy {
- UserInterestFeedAdapter(::onUserInterestNovelFeedClick)
- }
-
private val recommendedNovelsByUserTasteAdapter: RecommendedNovelsByUserTasteAdapter by lazy {
RecommendedNovelsByUserTasteAdapter(::onRecommendedNovelClick)
}
@@ -105,10 +103,10 @@ class HomeFragment : BaseFragment(fragment_home) {
setupItemDecoration()
setupObserver()
setupDotsIndicator()
- onPostInterestNovelClick()
onSettingPreferenceGenreClick()
onNotificationButtonClick()
onNormalSearchButtonClick()
+ onDetailSearchButtonClick()
tracker.trackEvent("home")
}
@@ -120,6 +118,14 @@ class HomeFragment : BaseFragment(fragment_home) {
}
}
+ private fun onDetailSearchButtonClick() {
+ binding.clHomeDetailSearch.setOnClickListener {
+ singleEventHandler.throttleFirst {
+ startActivity(DetailExploreActivity.getIntent(requireContext()))
+ }
+ }
+ }
+
private fun bindViewModel() {
binding.viewModel = homeViewModel
binding.lifecycleOwner = this
@@ -129,28 +135,17 @@ class HomeFragment : BaseFragment(fragment_home) {
with(binding) {
rvHomeTodayPopularNovel.adapter = popularNovelsAdapter
vpHomePopularFeed.adapter = popularFeedsAdapter
- rvUserInterestFeed.adapter = userInterestFeedAdapter
rvRecommendNovelByUserTaste.adapter = recommendedNovelsByUserTasteAdapter
}
}
private fun setupItemDecoration() {
- with(binding) {
- rvHomeTodayPopularNovel.addItemDecoration(
- HomeCustomItemDecoration(
- TODAY_POPULAR_NOVEL_MARGIN,
- ),
- )
- rvUserInterestFeed.addItemDecoration(HomeCustomItemDecoration(USER_INTEREST_MARGIN))
- }
+ binding.rvHomeTodayPopularNovel.addItemDecoration(
+ HomeCustomItemDecoration(TODAY_POPULAR_NOVEL_MARGIN),
+ )
}
private fun setupObserver() {
- mainViewModel.mainUiState.observe(viewLifecycleOwner) { uiState ->
- binding.tvHomeInterestFeed.text =
- getString(home_nickname_interest_feed, uiState.nickname)
- }
-
homeViewModel.uiState.observe(viewLifecycleOwner) { uiState ->
when {
uiState.error -> {
@@ -162,9 +157,7 @@ class HomeFragment : BaseFragment(fragment_home) {
binding.wllHome.setWebsosoLoadingVisibility(false)
popularNovelsAdapter.submitList(uiState.popularNovels)
popularFeedsAdapter.submitList(uiState.popularFeeds)
- updateUserInterestFeedsVisibility(uiState.userInterestFeeds.isEmpty())
updateRecommendedNovelByUserTasteVisibility(uiState.recommendedNovelsByUserTaste.isEmpty())
- userInterestFeedAdapter.submitList(uiState.userInterestFeeds)
recommendedNovelsByUserTasteAdapter.submitList(uiState.recommendedNovelsByUserTaste)
updateHasNotificationUnread(uiState.isNotificationUnread)
}
@@ -193,32 +186,6 @@ class HomeFragment : BaseFragment(fragment_home) {
}
}
- private fun updateUserInterestFeedsVisibility(isUserInterestEmpty: Boolean) {
- with(binding) {
- if (isUserInterestEmpty) {
- when (homeViewModel.uiState.value?.isInterestNovel) {
- true -> {
- clHomeUserInterestFeed.visibility = View.GONE
- clHomeInterestFeed.visibility = View.GONE
- clHomeNoAssociatedFeed.visibility = View.VISIBLE
- }
-
- false -> {
- clHomeUserInterestFeed.visibility = View.GONE
- clHomeInterestFeed.visibility = View.VISIBLE
- clHomeNoAssociatedFeed.visibility = View.GONE
- }
-
- else -> Unit
- }
- } else {
- clHomeUserInterestFeed.visibility = View.VISIBLE
- clHomeInterestFeed.visibility = View.GONE
- clHomeNoAssociatedFeed.visibility = View.GONE
- }
- }
- }
-
private fun updateRecommendedNovelByUserTasteVisibility(isRecommendNovelByUserTasteEmpty: Boolean) {
with(binding) {
if (isRecommendNovelByUserTasteEmpty) {
@@ -268,11 +235,6 @@ class HomeFragment : BaseFragment(fragment_home) {
navigateToNovelDetail(novelId)
}
- private fun onUserInterestNovelFeedClick(novelId: Long) {
- tracker.trackEvent("home_love_feedlist")
- navigateToNovelDetail(novelId)
- }
-
private fun onRecommendedNovelClick(novelId: Long) {
tracker.trackEvent("home_prefer_novellist")
navigateToNovelDetail(novelId)
@@ -301,17 +263,6 @@ class HomeFragment : BaseFragment(fragment_home) {
)
}
- private fun onPostInterestNovelClick() {
- binding.clHomeInterestFeed.setOnClickListener {
- tracker.trackEvent("home_to_love_btn")
- startActivityLauncher.launch(
- NormalExploreActivity.getIntent(
- requireContext(),
- ),
- )
- }
- }
-
private fun onSettingPreferenceGenreClick() {
binding.clHomeRecommendNovel.setOnClickListener {
tracker.trackEvent("home_to_prefer_btn")
@@ -341,7 +292,6 @@ class HomeFragment : BaseFragment(fragment_home) {
companion object {
private const val TODAY_POPULAR_NOVEL_MARGIN = 15
- private const val USER_INTEREST_MARGIN = 14
const val TAG = "HomeFragment"
}
}
diff --git a/app/src/main/java/com/into/websoso/ui/main/home/HomeViewModel.kt b/app/src/main/java/com/into/websoso/ui/main/home/HomeViewModel.kt
index 99c2f6ea0..c17f6ffe0 100644
--- a/app/src/main/java/com/into/websoso/ui/main/home/HomeViewModel.kt
+++ b/app/src/main/java/com/into/websoso/ui/main/home/HomeViewModel.kt
@@ -9,9 +9,6 @@ import com.into.websoso.data.model.PopularFeedsEntity
import com.into.websoso.data.model.PopularNovelsEntity
import com.into.websoso.data.model.RecommendedNovelsByUserTasteEntity
import com.into.websoso.data.model.TermsAgreementEntity
-import com.into.websoso.data.model.UserInterestFeedMessage
-import com.into.websoso.data.model.UserInterestFeedMessage.NO_INTEREST_NOVELS
-import com.into.websoso.data.model.UserInterestFeedsEntity
import com.into.websoso.data.repository.FeedRepository
import com.into.websoso.data.repository.NotificationRepository
import com.into.websoso.data.repository.NovelRepository
@@ -78,11 +75,9 @@ class HomeViewModel
val results = listOf(
async { runCatching { novelRepository.fetchPopularNovels() } },
async { runCatching { feedRepository.fetchPopularFeeds() } },
- async { runCatching { feedRepository.fetchUserInterestFeeds() } },
async { runCatching { novelRepository.fetchRecommendedNovelsByUserTaste() } },
).awaitAll()
- // 실패가 하나라도 있다면 상위 onFailure로 예외 전파
val failures = results.filter { it.isFailure }
if (failures.isNotEmpty()) {
throw failures.first().exceptionOrNull()
@@ -93,10 +88,8 @@ class HomeViewModel
?: PopularNovelsEntity(emptyList())
val popularFeeds = results[1].getOrNull() as? PopularFeedsEntity
?: PopularFeedsEntity(emptyList())
- val userInterestFeeds = results[2].getOrNull() as? UserInterestFeedsEntity
- ?: UserInterestFeedsEntity(emptyList(), "")
val recommendedNovels =
- results[3].getOrNull() as? RecommendedNovelsByUserTasteEntity
+ results[2].getOrNull() as? RecommendedNovelsByUserTasteEntity
?: RecommendedNovelsByUserTasteEntity(emptyList())
_uiState.value = uiState.value?.copy(
@@ -104,8 +97,6 @@ class HomeViewModel
error = false,
popularNovels = popularNovels.popularNovels,
popularFeeds = popularFeeds.popularFeeds.chunked(3),
- isInterestNovel = isUserInterestedInNovels(userInterestFeeds.message),
- userInterestFeeds = userInterestFeeds.userInterestFeeds,
recommendedNovelsByUserTaste = recommendedNovels.tasteNovels,
)
}.onFailure {
@@ -165,23 +156,13 @@ class HomeViewModel
fun updateFeed() {
viewModelScope.launch {
runCatching {
- listOf(
- async { feedRepository.fetchPopularFeeds() },
- async { feedRepository.fetchUserInterestFeeds() },
- ).awaitAll()
- }.onSuccess { responses ->
- val popularFeeds = responses[0] as PopularFeedsEntity
- val userInterestFeeds = responses[1] as UserInterestFeedsEntity
-
+ feedRepository.fetchPopularFeeds()
+ }.onSuccess { popularFeeds ->
_uiState.value = uiState.value?.copy(
popularFeeds = popularFeeds.popularFeeds.chunked(3),
- isInterestNovel = isUserInterestedInNovels(userInterestFeeds.message),
- userInterestFeeds = userInterestFeeds.userInterestFeeds,
)
}.onFailure {
- _uiState.value = uiState.value?.copy(
- error = true,
- )
+ _uiState.value = uiState.value?.copy(error = true)
}
}
}
@@ -225,12 +206,6 @@ class HomeViewModel
}
}
- private fun isUserInterestedInNovels(userInterestFeedMessage: String): Boolean =
- when (UserInterestFeedMessage.fromMessage(userInterestFeedMessage)) {
- NO_INTEREST_NOVELS -> false
- else -> true
- }
-
fun saveFCMToken(token: String) {
viewModelScope.launch {
runCatching {
diff --git a/app/src/main/java/com/into/websoso/ui/main/home/adpater/UserInterestFeedAdapter.kt b/app/src/main/java/com/into/websoso/ui/main/home/adpater/UserInterestFeedAdapter.kt
deleted file mode 100644
index 6c3716f2c..000000000
--- a/app/src/main/java/com/into/websoso/ui/main/home/adpater/UserInterestFeedAdapter.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-package com.into.websoso.ui.main.home.adpater
-
-import android.view.ViewGroup
-import androidx.recyclerview.widget.DiffUtil
-import androidx.recyclerview.widget.ListAdapter
-import androidx.recyclerview.widget.RecyclerView
-import com.into.websoso.data.model.UserInterestFeedsEntity.UserInterestFeedEntity
-
-class UserInterestFeedAdapter(
- private val onUserInterestFeedClick: (novelId: Long) -> (Unit),
-) : ListAdapter(diffUtil) {
- override fun onCreateViewHolder(
- parent: ViewGroup,
- viewType: Int,
- ): UserInterestFeedViewHolder = UserInterestFeedViewHolder.of(parent, onUserInterestFeedClick)
-
- override fun onBindViewHolder(
- holder: UserInterestFeedViewHolder,
- position: Int,
- ) {
- holder.bind(getItem(position))
-
- val params = holder.itemView.layoutParams as RecyclerView.LayoutParams
- params.width = (holder.itemView.context.resources.displayMetrics.widthPixels * 0.8).toInt()
- holder.itemView.layoutParams = params
- }
-
- companion object {
- val diffUtil = object : DiffUtil.ItemCallback() {
- override fun areItemsTheSame(
- oldItem: UserInterestFeedEntity,
- newItem: UserInterestFeedEntity,
- ): Boolean = oldItem.novelId == newItem.novelId
-
- override fun areContentsTheSame(
- oldItem: UserInterestFeedEntity,
- newItem: UserInterestFeedEntity,
- ): Boolean = oldItem == newItem
- }
- }
-}
diff --git a/app/src/main/java/com/into/websoso/ui/main/home/adpater/UserInterestFeedViewHolder.kt b/app/src/main/java/com/into/websoso/ui/main/home/adpater/UserInterestFeedViewHolder.kt
deleted file mode 100644
index b1dfa8aed..000000000
--- a/app/src/main/java/com/into/websoso/ui/main/home/adpater/UserInterestFeedViewHolder.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-package com.into.websoso.ui.main.home.adpater
-
-import android.view.LayoutInflater
-import android.view.ViewGroup
-import androidx.recyclerview.widget.RecyclerView
-import com.into.websoso.core.common.util.getS3ImageUrl
-import com.into.websoso.data.model.UserInterestFeedsEntity.UserInterestFeedEntity
-import com.into.websoso.databinding.ItemUserInterestFeedBinding
-
-class UserInterestFeedViewHolder(
- private val binding: ItemUserInterestFeedBinding,
- onUserInterestFeedClick: (novelId: Long) -> Unit,
-) : RecyclerView.ViewHolder(binding.root) {
- init {
- binding.onClick = onUserInterestFeedClick
- }
-
- fun bind(userInterestFeed: UserInterestFeedEntity) {
- val updateUserInterestFeed = userInterestFeed.copy(
- avatarImage = itemView.getS3ImageUrl(userInterestFeed.avatarImage ?: ""),
- )
- binding.userInterestFeed = updateUserInterestFeed
- }
-
- companion object {
- fun of(
- parent: ViewGroup,
- onUserInterestFeedClick: (novelId: Long) -> Unit,
- ): UserInterestFeedViewHolder {
- val binding = ItemUserInterestFeedBinding.inflate(
- LayoutInflater.from(parent.context),
- parent,
- false,
- )
- return UserInterestFeedViewHolder(binding, onUserInterestFeedClick)
- }
- }
-}
diff --git a/app/src/main/java/com/into/websoso/ui/main/home/model/HomeUiState.kt b/app/src/main/java/com/into/websoso/ui/main/home/model/HomeUiState.kt
index 609a79057..e3a5d451f 100644
--- a/app/src/main/java/com/into/websoso/ui/main/home/model/HomeUiState.kt
+++ b/app/src/main/java/com/into/websoso/ui/main/home/model/HomeUiState.kt
@@ -3,15 +3,12 @@ package com.into.websoso.ui.main.home.model
import com.into.websoso.data.model.PopularFeedsEntity.PopularFeedEntity
import com.into.websoso.data.model.PopularNovelsEntity.PopularNovelEntity
import com.into.websoso.data.model.RecommendedNovelsByUserTasteEntity.RecommendedNovelByUserTasteEntity
-import com.into.websoso.data.model.UserInterestFeedsEntity.UserInterestFeedEntity
data class HomeUiState(
val loading: Boolean = true,
val error: Boolean = false,
- val isInterestNovel: Boolean = false,
val isNotificationUnread: Boolean = false,
val popularNovels: List = listOf(),
val popularFeeds: List> = listOf(),
- val userInterestFeeds: List = listOf(),
val recommendedNovelsByUserTaste: List = listOf(),
)
diff --git a/app/src/main/java/com/into/websoso/ui/normalExplore/NormalExploreActivity.kt b/app/src/main/java/com/into/websoso/ui/normalExplore/NormalExploreActivity.kt
index a4c77c655..a55c9b2aa 100644
--- a/app/src/main/java/com/into/websoso/ui/normalExplore/NormalExploreActivity.kt
+++ b/app/src/main/java/com/into/websoso/ui/normalExplore/NormalExploreActivity.kt
@@ -17,6 +17,7 @@ import com.into.websoso.core.common.util.SingleEventHandler
import com.into.websoso.core.common.util.tracker.Tracker
import com.into.websoso.core.resource.R.string.novel_inquire_link
import com.into.websoso.databinding.ActivityNormalExploreBinding
+import com.into.websoso.ui.main.explore.adapter.SosoPickAdapter
import com.into.websoso.ui.normalExplore.adapter.NormalExploreAdapter
import com.into.websoso.ui.normalExplore.adapter.NormalExploreItemType.Header
import com.into.websoso.ui.normalExplore.adapter.NormalExploreItemType.Loading
@@ -37,6 +38,7 @@ class NormalExploreActivity : BaseActivity(activit
::navigateToInquire,
)
}
+ private val sosoPickAdapter: SosoPickAdapter by lazy { SosoPickAdapter(::navigateToNovelDetailFromSosoPick) }
private val normalExploreViewModel: NormalExploreViewModel by viewModels()
private val singleEventHandler: SingleEventHandler by lazy { SingleEventHandler.from() }
@@ -67,6 +69,7 @@ class NormalExploreActivity : BaseActivity(activit
),
)
}
+ rvNormalExploreSosoPick.adapter = sosoPickAdapter
onClick = onNormalExploreButtonClick()
}
}
@@ -153,6 +156,14 @@ class NormalExploreActivity : BaseActivity(activit
}
}
+ private fun navigateToNovelDetailFromSosoPick(novelId: Long) {
+ singleEventHandler.throttleFirst {
+ tracker.trackEvent("soso_pick", mapOf("novelId" to novelId))
+ val intent = NovelDetailActivity.getIntent(this, novelId)
+ startActivity(intent)
+ }
+ }
+
private fun navigateToInquire() {
val inquireUrl = getString(novel_inquire_link)
val intent = Intent(ACTION_VIEW, Uri.parse(inquireUrl))
@@ -183,6 +194,10 @@ class NormalExploreActivity : BaseActivity(activit
normalExploreViewModel.searchWord.observe(this) {
normalExploreViewModel.validateSearchWordClearButton()
}
+
+ normalExploreViewModel.sosoPicks.observe(this) { sosoPicks ->
+ sosoPickAdapter.submitList(sosoPicks)
+ }
}
private fun updateView(uiState: NormalExploreUiState) {
diff --git a/app/src/main/java/com/into/websoso/ui/normalExplore/NormalExploreViewModel.kt b/app/src/main/java/com/into/websoso/ui/normalExplore/NormalExploreViewModel.kt
index 47972292c..5ee3a74a8 100644
--- a/app/src/main/java/com/into/websoso/ui/normalExplore/NormalExploreViewModel.kt
+++ b/app/src/main/java/com/into/websoso/ui/normalExplore/NormalExploreViewModel.kt
@@ -5,6 +5,8 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
+import com.into.websoso.data.model.SosoPickEntity
+import com.into.websoso.data.repository.NovelRepository
import com.into.websoso.domain.usecase.GetNormalExploreResultUseCase
import com.into.websoso.ui.mapper.toUi
import com.into.websoso.ui.normalExplore.NormalExploreActivity.Companion.SEARCH_AUTHOR
@@ -18,6 +20,7 @@ class NormalExploreViewModel
@Inject
constructor(
private val getNormalExploreResultUseCase: GetNormalExploreResultUseCase,
+ private val novelRepository: NovelRepository,
private val savedStateHandle: SavedStateHandle,
) : ViewModel() {
private val _uiState: MutableLiveData =
@@ -36,12 +39,29 @@ class NormalExploreViewModel
private val _isNovelResultEmptyBoxVisibility: MutableLiveData = MutableLiveData(false)
val isNovelResultEmptyBoxVisibility: LiveData get() = _isNovelResultEmptyBoxVisibility
+ private val _sosoPicks: MutableLiveData> = MutableLiveData(emptyList())
+ val sosoPicks: LiveData> get() = _sosoPicks
+
+ private val _isSosoPickVisible: MutableLiveData = MutableLiveData(initialSearchWord.isBlank())
+ val isSosoPickVisible: LiveData get() = _isSosoPickVisible
+
init {
+ fetchSosoPicks()
if (initialSearchWord.isNotBlank()) {
updateSearchResult(isSearchButtonClick = true)
}
}
+ private fun fetchSosoPicks() {
+ viewModelScope.launch {
+ runCatching {
+ novelRepository.fetchSosoPicks()
+ }.onSuccess { result ->
+ _sosoPicks.value = result.novels
+ }
+ }
+ }
+
fun updateSearchWord(searchWord: String) {
_searchWord.value = searchWord
savedStateHandle[SEARCH_AUTHOR] = searchWord
@@ -51,6 +71,9 @@ class NormalExploreViewModel
if ((_searchWord.value.isNullOrBlank() || _uiState.value?.isLoadable == false) && !isSearchButtonClick) {
return
}
+ if (isSearchButtonClick) {
+ _isSosoPickVisible.value = false
+ }
viewModelScope.launch {
_uiState.value = _uiState.value?.copy(loading = isSearchButtonClick)
runCatching {
@@ -90,5 +113,6 @@ class NormalExploreViewModel
fun updateSearchWordEmpty() {
_searchWord.value = ""
savedStateHandle[SEARCH_AUTHOR] = ""
+ _isSosoPickVisible.value = true
}
}
diff --git a/app/src/main/java/com/into/websoso/ui/novelFeed/NovelFeedFragment.kt b/app/src/main/java/com/into/websoso/ui/novelFeed/NovelFeedFragment.kt
index b095262f1..35b7577cf 100644
--- a/app/src/main/java/com/into/websoso/ui/novelFeed/NovelFeedFragment.kt
+++ b/app/src/main/java/com/into/websoso/ui/novelFeed/NovelFeedFragment.kt
@@ -5,7 +5,6 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import android.view.ViewGroup.LayoutParams
import android.view.WindowManager.LayoutParams.WRAP_CONTENT
import android.widget.PopupWindow
import android.widget.TextView
@@ -341,17 +340,8 @@ class NovelFeedFragment : BaseFragment(R.layout.fragme
}
private fun setupRefreshView() {
- binding.sptrNovelFeedRefresh.apply {
- setRefreshViewParams(
- params = LayoutParams(
- 30.toIntPxFromDp(),
- 30.toIntPxFromDp(),
- ),
- )
- setLottieAnimation("lottie_websoso_loading.json")
- setOnRefreshListener {
- novelFeedViewModel.updateRefreshedFeeds(novelId)
- }
+ binding.sptrNovelFeedRefresh.setOnRefreshListener {
+ novelFeedViewModel.updateRefreshedFeeds(novelId)
}
}
@@ -375,7 +365,7 @@ class NovelFeedFragment : BaseFragment(R.layout.fragme
!novelFeedUiState.loading -> {
binding.wllNovelFeed.setWebsosoLoadingVisibility(false)
- binding.sptrNovelFeedRefresh.setRefreshing(false)
+ binding.sptrNovelFeedRefresh.isRefreshing = false
updateFeeds(novelFeedUiState)
}
}
diff --git a/app/src/main/res/layout/activity_normal_explore.xml b/app/src/main/res/layout/activity_normal_explore.xml
index 26f1d334a..50c13ecb5 100644
--- a/app/src/main/res/layout/activity_normal_explore.xml
+++ b/app/src/main/res/layout/activity_normal_explore.xml
@@ -75,6 +75,7 @@
android:autofillHints="@null"
android:background="@null"
android:cursorVisible="true"
+ android:hint="@string/explore_normal_search_hint"
android:imeOptions="actionSearch"
android:inputType="text"
android:maxLines="1"
@@ -83,6 +84,7 @@
android:textAppearance="@style/label1"
android:textColor="@color/black"
android:textColorHighlight="@color/primary_50_F1EFFF"
+ android:textColorHint="@color/gray_200_949399"
android:theme="@style/Theme.Websoso.EditText"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/iv_normal_explore_cancel_button"
@@ -123,6 +125,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/fragment_detail_explore_info.xml b/app/src/main/res/layout/fragment_detail_explore_info.xml
deleted file mode 100644
index f902b4aa6..000000000
--- a/app/src/main/res/layout/fragment_detail_explore_info.xml
+++ /dev/null
@@ -1,184 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/fragment_detail_explore_keyword.xml b/app/src/main/res/layout/fragment_detail_explore_keyword.xml
deleted file mode 100644
index 6e43cc3c6..000000000
--- a/app/src/main/res/layout/fragment_detail_explore_keyword.xml
+++ /dev/null
@@ -1,256 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/fragment_detail_explore_result_info.xml b/app/src/main/res/layout/fragment_detail_explore_result_info.xml
deleted file mode 100644
index 6f93908cb..000000000
--- a/app/src/main/res/layout/fragment_detail_explore_result_info.xml
+++ /dev/null
@@ -1,182 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/fragment_detail_explore_result_keyword.xml b/app/src/main/res/layout/fragment_detail_explore_result_keyword.xml
deleted file mode 100644
index 7ace9a82b..000000000
--- a/app/src/main/res/layout/fragment_detail_explore_result_keyword.xml
+++ /dev/null
@@ -1,255 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/fragment_explore.xml b/app/src/main/res/layout/fragment_explore.xml
deleted file mode 100644
index 05cc959c8..000000000
--- a/app/src/main/res/layout/fragment_explore.xml
+++ /dev/null
@@ -1,242 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml
index b467f12bf..53ee70e1f 100644
--- a/app/src/main/res/layout/fragment_home.xml
+++ b/app/src/main/res/layout/fragment_home.xml
@@ -87,6 +87,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ app:layout_constraintTop_toBottomOf="@+id/cl_home_detail_search" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ app:layout_constraintTop_toBottomOf="@id/dotsIndicator_home" />
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/menu/menu_main_bnv.xml b/app/src/main/res/menu/menu_main_bnv.xml
index 1b19c67c9..825d8a2db 100644
--- a/app/src/main/res/menu/menu_main_bnv.xml
+++ b/app/src/main/res/menu/menu_main_bnv.xml
@@ -5,11 +5,6 @@
android:icon="@drawable/ic_main_home"
android:title="@string/main_ic_home" />
-
-
- ,
- @SerialName("message")
- val message: String,
-) {
- @Serializable
- data class UserInterestFeedResponseDto(
- @SerialName("avatarImage")
- val avatarImage: String,
- @SerialName("feedContent")
- val feedContent: String,
- @SerialName("nickname")
- val nickname: String,
- @SerialName("novelId")
- val novelId: Int,
- @SerialName("novelImage")
- val novelImage: String,
- @SerialName("novelRating")
- val novelRating: Double,
- @SerialName("novelRatingCount")
- val novelRatingCount: Int,
- @SerialName("novelTitle")
- val novelTitle: String,
- )
-}
diff --git a/core/resource/src/main/res/drawable-xxhdpi/img_home_detail_search.png b/core/resource/src/main/res/drawable-xxhdpi/img_home_detail_search.png
new file mode 100644
index 000000000..304ca738a
Binary files /dev/null and b/core/resource/src/main/res/drawable-xxhdpi/img_home_detail_search.png differ
diff --git a/core/resource/src/main/res/drawable/ic_main_explore.xml b/core/resource/src/main/res/drawable/ic_main_explore.xml
deleted file mode 100644
index 83e83c1d5..000000000
--- a/core/resource/src/main/res/drawable/ic_main_explore.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
diff --git a/core/resource/src/main/res/drawable/ic_plus_novel.xml b/core/resource/src/main/res/drawable/ic_plus_novel.xml
new file mode 100644
index 000000000..31e4fab63
--- /dev/null
+++ b/core/resource/src/main/res/drawable/ic_plus_novel.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/core/resource/src/main/res/values/strings.xml b/core/resource/src/main/res/values/strings.xml
index 364c8ca5b..3b156ac51 100644
--- a/core/resource/src/main/res/values/strings.xml
+++ b/core/resource/src/main/res/values/strings.xml
@@ -7,7 +7,6 @@
홈
- 탐색
서재
피드
My
@@ -21,11 +20,9 @@
찾는 작품이 없다면?
- 탐색하기
작품 제목, 작가를 검색하세요
뭐 읽을지 고민될 땐?
장르, 연재상태, 별점, 키워드로 작품 찾기
- 내 취향에 맞는 웹소설 찾기
소소
다른 독자들이 최근에 찾아본 웹소설이에요
@@ -46,6 +43,8 @@
%1$.1f이상
%1$.1f이상
%1$.1f이상
+ %1$.1f ~ %2$.1f
+ %1$.1f
키워드를 검색하세요
@@ -234,10 +233,6 @@
+ 오늘의 발견 +
지금 뜨는 글
- 관심글
- 관심 등록한 작품의 최신 글이에요
- 관심작품의 최신 소식을 모아서 볼 수 있어요.\n좋아하는 웹소설을 관심 등록 해볼까요?
- 관심작품 등록하기
관심 등록한 작품의 최신 글이에요
로맨스, 로판, 판타지, 현판 등\n선호장르를 기반으로 웹소설을 추천해드려요!
선호장르를 기반으로 추천해드려요
@@ -246,12 +241,7 @@
%s님의 한마디
작품 소개
스포일러가 포함된 글 보기
- %1$s님의 한마디
- %1$.1f (%2$d)
- ・ :*%1$s님의 관심글*: ・
%1$.1f (%2$d)
- 관심 등록한 작품과 관련된 글이 없어요
- 관심글
설정
diff --git a/data/feed/src/main/java/com/into/websoso/data/feed/mapper/FeedMapper.kt b/data/feed/src/main/java/com/into/websoso/data/feed/mapper/FeedMapper.kt
index 6ffe13643..68ae94e9f 100644
--- a/data/feed/src/main/java/com/into/websoso/data/feed/mapper/FeedMapper.kt
+++ b/data/feed/src/main/java/com/into/websoso/data/feed/mapper/FeedMapper.kt
@@ -6,14 +6,12 @@ import com.into.websoso.core.network.datasource.feed.model.response.FeedDetailRe
import com.into.websoso.core.network.datasource.feed.model.response.FeedResponseDto
import com.into.websoso.core.network.datasource.feed.model.response.FeedsResponseDto
import com.into.websoso.core.network.datasource.feed.model.response.PopularFeedsResponseDto
-import com.into.websoso.core.network.datasource.feed.model.response.UserInterestFeedsResponseDto
import com.into.websoso.data.feed.model.CommentEntity
import com.into.websoso.data.feed.model.CommentsEntity
import com.into.websoso.data.feed.model.FeedDetailEntity
import com.into.websoso.data.feed.model.FeedEntity
import com.into.websoso.data.feed.model.FeedsEntity
import com.into.websoso.data.feed.model.PopularFeedsEntity
-import com.into.websoso.data.feed.model.UserInterestFeedsEntity
fun FeedsResponseDto.toData(): FeedsEntity =
FeedsEntity(
@@ -122,20 +120,3 @@ fun PopularFeedsResponseDto.toData(): PopularFeedsEntity =
)
},
)
-
-fun UserInterestFeedsResponseDto.toData(): UserInterestFeedsEntity =
- UserInterestFeedsEntity(
- userInterestFeeds = userInterestFeeds.map { feed ->
- UserInterestFeedsEntity.UserInterestFeedEntity(
- avatarImage = feed.avatarImage,
- feedContent = feed.feedContent,
- nickname = feed.nickname,
- novelId = feed.novelId,
- novelImage = feed.novelImage,
- novelRating = feed.novelRating,
- novelRatingCount = feed.novelRatingCount,
- novelTitle = feed.novelTitle,
- )
- },
- message = message,
- )
diff --git a/data/feed/src/main/java/com/into/websoso/data/feed/model/UserInterestFeedsEntity.kt b/data/feed/src/main/java/com/into/websoso/data/feed/model/UserInterestFeedsEntity.kt
deleted file mode 100644
index f34c9967d..000000000
--- a/data/feed/src/main/java/com/into/websoso/data/feed/model/UserInterestFeedsEntity.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-package com.into.websoso.data.feed.model
-
-data class UserInterestFeedsEntity(
- val userInterestFeeds: List,
- val message: String,
-) {
- data class UserInterestFeedEntity(
- val avatarImage: String,
- val feedContent: String,
- val nickname: String,
- val novelId: Int,
- val novelImage: String,
- val novelRating: Double,
- val novelRatingCount: Int,
- val novelTitle: String,
- )
-}
-
-enum class UserInterestFeedMessage(
- val message: String,
-) {
- NO_INTEREST_NOVELS("NO_INTEREST_NOVELS"),
- NO_ASSOCIATED_FEEDS("NO_ASSOCIATED_FEEDS"),
- ;
-
- companion object {
- fun fromMessage(message: String): UserInterestFeedMessage? = entries.find { it.message == message }
- }
-}
diff --git a/feature/library/src/main/java/com/into/websoso/feature/library/LibraryScreen.kt b/feature/library/src/main/java/com/into/websoso/feature/library/LibraryScreen.kt
index 81b8eb420..543452cb5 100644
--- a/feature/library/src/main/java/com/into/websoso/feature/library/LibraryScreen.kt
+++ b/feature/library/src/main/java/com/into/websoso/feature/library/LibraryScreen.kt
@@ -101,7 +101,7 @@ fun LibraryScreen(
onSortClick = libraryViewModel::updateSortType,
onToggleViewType = libraryViewModel::updateViewType,
onItemClick = { navigateToNovelDetailActivity(it.novelId) },
- onSearchClick = { /* TODO */ },
+ onSearchClick = navigateToNormalExploreActivity,
onExploreClick = navigateToNormalExploreActivity,
onInterestClick = libraryViewModel::updateInterestedNovels,
onAttractivePointClick = libraryViewModel::updateAttractivePoints,
diff --git a/feature/library/src/main/java/com/into/websoso/feature/library/component/LibraryTopBar.kt b/feature/library/src/main/java/com/into/websoso/feature/library/component/LibraryTopBar.kt
index b66081877..40f36fc25 100644
--- a/feature/library/src/main/java/com/into/websoso/feature/library/component/LibraryTopBar.kt
+++ b/feature/library/src/main/java/com/into/websoso/feature/library/component/LibraryTopBar.kt
@@ -1,23 +1,30 @@
package com.into.websoso.feature.library.component
+import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
+import com.into.websoso.core.common.extensions.debouncedClickable
import com.into.websoso.core.designsystem.theme.Black
import com.into.websoso.core.designsystem.theme.WebsosoTheme
+import com.into.websoso.core.resource.R.drawable.ic_plus_novel
@Composable
internal fun LibraryTopBar(onSearchClick: () -> Unit = {}) {
Row(
modifier = Modifier
.fillMaxWidth()
- .padding(horizontal = 20.dp),
+ .padding(start = 20.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
@@ -26,17 +33,16 @@ internal fun LibraryTopBar(onSearchClick: () -> Unit = {}) {
style = WebsosoTheme.typography.headline1,
color = Black,
)
- /*
- TODO: 추후 검색 기능 구현 시 롤백
- IconButton(onClick = onSearchClick) {
- Image(
- imageVector = ImageVector.vectorResource(id = ic_common_search),
- contentDescription = "검색",
- modifier = Modifier.size(24.dp),
- )
- }
- }
-
- */
+ Box(
+ modifier = Modifier
+ .debouncedClickable(onClick = onSearchClick)
+ .padding(horizontal = 20.dp, vertical = 8.dp),
+ ) {
+ Image(
+ imageVector = ImageVector.vectorResource(id = ic_plus_novel),
+ contentDescription = null,
+ modifier = Modifier.size(24.dp),
+ )
+ }
}
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 25217f871..d952d5fab 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,7 +1,7 @@
[versions]
# App Versioning
-versionCode = "10050"
-versionName = "1.6.3"
+versionCode = "10051"
+versionName = "1.7.0"
# Gradle Plugin & Kotlin
android-gradle-plugin = "8.13.2"
@@ -54,7 +54,7 @@ coil-transformers = "1.0.6"
# Misc UI Libraries
dots-indicator = "5.1.0"
lottie = "6.7.1"
-pull-to-refresh = "1.5.2"
+swipe-refresh-layout = "1.2.0"
# Social Login Libraries
kakao = "2.23.1"
@@ -130,7 +130,7 @@ coil-transformers = { module = "jp.wasabeef.transformers:coil", version.ref = "c
# Misc UI Libraries
dots-indicator = { module = "com.tbuonomo:dotsindicator", version.ref = "dots-indicator" }
lottie = { module = "com.airbnb.android:lottie", version.ref = "lottie" }
-pull-to-refresh = { module = "com.github.SimformSolutionsPvtLtd:SSPullToRefresh", version.ref = "pull-to-refresh" }
+swipe-refresh-layout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "swipe-refresh-layout" }
# Social Login Libraries
kakao = { module = "com.kakao.sdk:v2-user", version.ref = "kakao" }