Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion app/src/main/java/com/into/websoso/data/mapper/FeedMapper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,14 @@ fun PopularFeedsResponseDto.toData(): PopularFeedsEntity =
popularFeeds = popularFeeds.map { feed ->
PopularFeedsEntity.PopularFeedEntity(
feedId = feed.feedId,
feesContent = feed.feedContent,
feesContent = feed.feedContent.orEmpty(),
likeCount = feed.likeCount,
commentCount = feed.commentCount,
isSpoiler = feed.isSpoiler,
isPublic = feed.isPublic ?: false,
novelTitle = feed.novelTitle ?: feed.title.orEmpty(),
novelImage = feed.novelImage ?: feed.novelThumbnailImage.orEmpty(),
novelGenreImage = feed.novelGenreImage.orEmpty(),
)
},
)
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,9 @@ data class PopularFeedsEntity(
val likeCount: Int,
val commentCount: Int,
val isSpoiler: Boolean,
val isPublic: Boolean,
val novelTitle: String,
val novelImage: String,
val novelGenreImage: String,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,24 @@ data class PopularFeedsResponseDto(
@SerialName("feedId")
val feedId: Long,
@SerialName("feedContent")
val feedContent: String,
val feedContent: String? = null,
@SerialName("likeCount")
val likeCount: Int,
@SerialName("commentCount")
val commentCount: Int,
@SerialName("isSpoiler")
val isSpoiler: Boolean,
@SerialName("isPublic")
val isPublic: Boolean? = null,
@SerialName("title")
val title: String? = null,
@SerialName("novelTitle")
val novelTitle: String? = null,
@SerialName("novelImage")
val novelImage: String? = null,
@SerialName("novelThumbnailImage")
val novelThumbnailImage: String? = null,
@SerialName("novelGenreImage")
val novelGenreImage: String? = null,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ 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.remote.api.FeedApi
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import javax.inject.Inject
import javax.inject.Singleton

Expand Down Expand Up @@ -35,6 +38,34 @@ class FeedRepository

suspend fun fetchPopularFeeds(): PopularFeedsEntity = feedApi.getPopularFeeds().toData()

suspend fun fetchPopularFeedsWithDetails(): List<PopularFeedsEntity.PopularFeedEntity> =
coroutineScope {
fetchPopularFeeds()
.popularFeeds
.map { feed ->
async {
runCatching {
val feedDetail = feedApi.getFeed(feed.feedId).toData()
val novel = feedDetail.novel ?: return@runCatching null

feed.copy(
feesContent = feedDetail.content,
isSpoiler = feedDetail.isSpoiler,
isPublic = feedDetail.isPublic,
novelTitle = novel.title,
novelImage = feed.novelImage.ifBlank { novel.thumbnail },
)
}.getOrNull()
}
}.awaitAll()
.filterNotNull()
.filter { feed ->
feed.isPublic &&
feed.novelTitle.isNotBlank() &&
feed.novelImage.isNotBlank()
}
}

suspend fun saveRemovedFeed(feedId: Long) {
runCatching {
feedApi.deleteFeed(feedId)
Expand Down
122 changes: 67 additions & 55 deletions app/src/main/java/com/into/websoso/ui/main/home/HomeViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
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
Expand All @@ -18,6 +17,7 @@ import com.into.websoso.ui.main.home.model.HomeUiState
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
Expand Down Expand Up @@ -69,42 +69,38 @@ class HomeViewModel
}
}

private fun fetchUserHomeData() {
viewModelScope.launch {
runCatching {
val results = listOf(
async { runCatching { novelRepository.fetchPopularNovels() } },
async { runCatching { feedRepository.fetchPopularFeeds() } },
async { runCatching { novelRepository.fetchRecommendedNovelsByUserTaste() } },
).awaitAll()

val failures = results.filter { it.isFailure }
if (failures.isNotEmpty()) {
throw failures.first().exceptionOrNull()
?: IllegalStateException("Unknown error")
}
private suspend fun fetchUserHomeData() {
coroutineScope {
val popularNovelsDeferred =
async { runCatching { novelRepository.fetchPopularNovels() } }
val popularFeedsDeferred =
async { runCatching { feedRepository.fetchPopularFeedsWithDetails() } }
val recommendedNovelsDeferred =
async { runCatching { novelRepository.fetchRecommendedNovelsByUserTaste() } }

val popularNovels = results[0].getOrNull() as? PopularNovelsEntity
?: PopularNovelsEntity(emptyList())
val popularFeeds = results[1].getOrNull() as? PopularFeedsEntity
?: PopularFeedsEntity(emptyList())
val recommendedNovels =
results[2].getOrNull() as? RecommendedNovelsByUserTasteEntity
?: RecommendedNovelsByUserTasteEntity(emptyList())
val popularNovelsResult = popularNovelsDeferred.await()
val popularFeedsResult = popularFeedsDeferred.await()
val recommendedNovelsResult = recommendedNovelsDeferred.await()

_uiState.value = uiState.value?.copy(
loading = false,
error = false,
popularNovels = popularNovels.popularNovels,
popularFeeds = popularFeeds.popularFeeds.chunked(3),
recommendedNovelsByUserTaste = recommendedNovels.tasteNovels,
)
}.onFailure {
_uiState.value = uiState.value?.copy(
loading = false,
error = true,
)
val failure = popularNovelsResult.exceptionOrNull()
?: popularFeedsResult.exceptionOrNull()
?: recommendedNovelsResult.exceptionOrNull()
if (failure != null) {
handleFailureState()
return@coroutineScope
}

val popularNovels = popularNovelsResult.getOrThrow()
val popularFeeds = popularFeedsResult.getOrThrow()
val recommendedNovels = recommendedNovelsResult.getOrThrow()

_uiState.value = uiState.value?.copy(
loading = false,
error = false,
popularNovels = popularNovels.popularNovels,
popularFeeds = popularFeeds.chunked(HOME_POPULAR_FEED_PAGE_SIZE),
recommendedNovelsByUserTaste = recommendedNovels.tasteNovels,
)
}
}

Expand All @@ -129,37 +125,49 @@ class HomeViewModel
}

private suspend fun fetchGuestData() {
viewModelScope.launch {
runCatching {
listOf(
async { novelRepository.fetchPopularNovels() },
async { feedRepository.fetchPopularFeeds() },
).awaitAll()
}.onSuccess { responses ->
val popularNovels = responses[0] as PopularNovelsEntity
val popularFeeds = responses[1] as PopularFeedsEntity
coroutineScope {
val popularNovelsDeferred =
async { runCatching { novelRepository.fetchPopularNovels() } }
val popularFeedsDeferred =
async { runCatching { feedRepository.fetchPopularFeedsWithDetails() } }

_uiState.value = uiState.value?.copy(
loading = false,
popularNovels = popularNovels.popularNovels,
popularFeeds = popularFeeds.popularFeeds.chunked(3),
)
}.onFailure {
_uiState.value = uiState.value?.copy(
loading = false,
error = true,
)
val popularNovelsResult = popularNovelsDeferred.await()
val popularFeedsResult = popularFeedsDeferred.await()

val failure = popularNovelsResult.exceptionOrNull()
?: popularFeedsResult.exceptionOrNull()
if (failure != null) {
handleFailureState()
return@coroutineScope
}

val popularNovels = popularNovelsResult.getOrThrow()
val popularFeeds = popularFeedsResult.getOrThrow()

_uiState.value = uiState.value?.copy(
loading = false,
error = false,
popularNovels = popularNovels.popularNovels,
popularFeeds = popularFeeds.chunked(HOME_POPULAR_FEED_PAGE_SIZE),
)
}
}

private fun handleFailureState() {
_uiState.value = uiState.value?.copy(
loading = false,
error = true,
)
}

fun updateFeed() {
viewModelScope.launch {
runCatching {
feedRepository.fetchPopularFeeds()
feedRepository.fetchPopularFeedsWithDetails()
}.onSuccess { popularFeeds ->
_uiState.value = uiState.value?.copy(
popularFeeds = popularFeeds.popularFeeds.chunked(3),
error = false,
popularFeeds = popularFeeds.chunked(HOME_POPULAR_FEED_PAGE_SIZE),
)
}.onFailure {
_uiState.value = uiState.value?.copy(error = true)
Expand Down Expand Up @@ -251,4 +259,8 @@ class HomeViewModel
}
}
}

companion object {
private const val HOME_POPULAR_FEED_PAGE_SIZE = 2
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class PopularFeedsAdapter(
override fun areItemsTheSame(
oldItem: List<PopularFeedEntity>,
newItem: List<PopularFeedEntity>,
): Boolean = oldItem.firstOrNull()?.feedId == newItem.firstOrNull()?.feedId
): Boolean = oldItem.map { it.feedId } == newItem.map { it.feedId }

@SuppressLint("DiffUtilEquals")
override fun areContentsTheSame(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
package com.into.websoso.ui.main.home.adpater

import android.util.Patterns
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import coil.decode.SvgDecoder
import coil.load
import coil.transform.RoundedCornersTransformation
import com.into.websoso.core.common.util.getS3ImageUrl
import com.into.websoso.core.common.util.toFloatPxFromDp
import com.into.websoso.core.resource.R.drawable.img_loading_thumbnail
import com.into.websoso.data.model.PopularFeedsEntity.PopularFeedEntity
import com.into.websoso.databinding.ItemPopularFeedBinding
import com.into.websoso.databinding.ItemPopularFeedSlotBinding

class PopularFeedsViewHolder(
private val binding: ItemPopularFeedBinding,
Expand All @@ -13,24 +21,63 @@ class PopularFeedsViewHolder(
listOf(
binding.itemPopularFeesSlot1,
binding.itemPopularFeesSlot2,
binding.itemPopularFeesSlot3,
)
}

fun bind(feedItems: List<PopularFeedEntity>) {
binding.viewPopularFeedDivider1.visibility =
if (feedItems.size > 1) View.VISIBLE else View.GONE

slots.forEachIndexed { index, slotBinding ->
slotBinding.apply {
feedItems.getOrNull(index)?.let { feed ->
tvPopularFeedContent.text = feed.feesContent
tvPopularFeedLikeCount.text = feed.likeCount.toString()
tvPopularFeedCommentCount.text = feed.commentCount.toString()
tvPopularFeedContent.visibility =
if (feed.isSpoiler) View.INVISIBLE else View.VISIBLE
tvPopularFeedContentSpoiler.visibility =
if (feed.isSpoiler) View.VISIBLE else View.GONE
root.setOnClickListener { onFeedClick(feed.feedId) }
}
val feed = feedItems.getOrNull(index)
if (feed == null) {
slotBinding.root.visibility = View.INVISIBLE
slotBinding.root.setOnClickListener(null)
return@forEachIndexed
}

slotBinding.root.visibility = View.VISIBLE
slotBinding.bind(feed)
}
}

private fun ItemPopularFeedSlotBinding.bind(feed: PopularFeedEntity) {
tvPopularFeedTitle.text = feed.novelTitle.ellipsizeByLength()
tvPopularFeedContent.text = if (feed.isSpoiler) "" else feed.feesContent
tvPopularFeedContent.visibility = if (feed.isSpoiler) View.GONE else View.VISIBLE
tvPopularFeedContentSpoiler.visibility = if (feed.isSpoiler) View.VISIBLE else View.GONE
ivPopularFeedThumbnail.load(feed.novelImage.toImageUrl()) {
crossfade(true)
transformations(RoundedCornersTransformation(8f.toFloatPxFromDp()))
error(img_loading_thumbnail)
}
val isGenreVisible = feed.novelGenreImage.isNotBlank()
ivPopularFeedGenreFrame.visibility = if (isGenreVisible) View.VISIBLE else View.GONE
ivPopularFeedGenre.visibility = if (isGenreVisible) View.VISIBLE else View.GONE
if (isGenreVisible) {
ivPopularFeedGenre.load(feed.novelGenreImage.toImageUrl()) {
crossfade(true)
decoderFactory(SvgDecoder.Factory())
}
}
root.setOnClickListener { onFeedClick(feed.feedId) }
}

private fun String.toImageUrl(): String =
when {
isBlank() -> ""
Patterns.WEB_URL.matcher(this).matches() -> this
else -> itemView.getS3ImageUrl(this)
}

private fun String.ellipsizeByLength(): String =
if (length > MAX_TITLE_LENGTH) {
take(MAX_TITLE_LENGTH) + "..."
} else {
this
}

companion object {
private const val MAX_TITLE_LENGTH = 16
}
}
2 changes: 1 addition & 1 deletion app/src/main/res/layout/fragment_home.xml
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/vp_home_popular_feed"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="244dp"
android:layout_marginHorizontal="20dp"
android:layout_marginTop="14dp"
android:background="@drawable/bg_home_white_stroke_gray_70_radius_14dp"
Expand Down
Loading
Loading