Show updated manga on top of feed

This commit is contained in:
Koitharu
2023-08-11 13:18:23 +03:00
parent 788c7b862a
commit 0271ed2ba9
11 changed files with 133 additions and 30 deletions

View File

@@ -8,6 +8,7 @@ enum class ListItemType {
MANGA_LIST,
MANGA_LIST_DETAILED,
MANGA_GRID,
MANGA_NESTED_GROUP,
FOOTER_LOADING,
FOOTER_ERROR,
STATE_LOADING,

View File

@@ -44,11 +44,12 @@ class TypedListSpacingDecoration(
ListItemType.EXPLORE_SOURCE_GRID,
ListItemType.EXPLORE_SOURCE_LIST,
ListItemType.EXPLORE_SUGGESTION,
ListItemType.MANGA_NESTED_GROUP,
null -> outRect.set(0)
ListItemType.TIP -> outRect.set(0) // TODO
ListItemType.HINT_EMPTY -> outRect.set(0) // TODO
ListItemType.FEED -> outRect.set(0) // TODO
ListItemType.FEED -> outRect.set(spacingList, 0, spacingList, 0)
}
}

View File

@@ -47,8 +47,8 @@ fun searchResultsAD(
bind {
binding.textViewTitle.text = item.source.title
binding.buttonMore.isVisible = item.hasMore
adapter.notifyDataSetChanged()
adapter.items = item.list
adapter.notifyDataSetChanged()
binding.recyclerView.isGone = item.list.isEmpty()
binding.textViewError.textAndVisible = item.error?.getDisplayMessage(context.resources)
}

View File

@@ -37,6 +37,10 @@ abstract class TracksDao {
@Query("SELECT manga.* FROM tracks LEFT JOIN manga ON manga.manga_id = tracks.manga_id WHERE chapters_new > 0 ORDER BY chapters_new DESC")
abstract fun observeUpdatedManga(): Flow<List<MangaWithTags>>
@Transaction
@Query("SELECT manga.* FROM tracks LEFT JOIN manga ON manga.manga_id = tracks.manga_id WHERE chapters_new > 0 ORDER BY chapters_new DESC LIMIT :limit")
abstract fun observeUpdatedManga(limit: Int): Flow<List<MangaWithTags>>
@Query("DELETE FROM tracks")
abstract suspend fun clear()

View File

@@ -49,9 +49,12 @@ class TrackingRepository @Inject constructor(
.onStart { gcIfNotCalled() }
}
fun observeUpdatedManga(): Flow<List<Manga>> {
return db.tracksDao.observeUpdatedManga()
.mapItems { it.toManga() }
fun observeUpdatedManga(limit: Int = 0): Flow<List<Manga>> {
return if (limit == 0) {
db.tracksDao.observeUpdatedManga()
} else {
db.tracksDao.observeUpdatedManga(limit)
}.mapItems { it.toManga() }
.distinctUntilChanged()
.onStart { gcIfNotCalled() }
}

View File

@@ -24,10 +24,12 @@ import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.size.StaticItemSizeResolver
import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.tracker.ui.feed.adapter.FeedAdapter
import org.koitharu.kotatsu.tracker.ui.updates.UpdatesActivity
import javax.inject.Inject
@AndroidEntryPoint
@@ -50,7 +52,8 @@ class FeedFragment :
override fun onViewBindingCreated(binding: FragmentFeedBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
feedAdapter = FeedAdapter(coil, viewLifecycleOwner, this)
val sizeResolver = StaticItemSizeResolver(resources.getDimensionPixelSize(R.dimen.smaller_grid_width))
feedAdapter = FeedAdapter(coil, viewLifecycleOwner, this, sizeResolver)
with(binding.recyclerView) {
adapter = feedAdapter
setHasFixedSize(true)
@@ -96,7 +99,10 @@ class FeedFragment :
override fun onEmptyActionClick() = Unit
override fun onListHeaderClick(item: ListHeader, view: View) = Unit
override fun onListHeaderClick(item: ListHeader, view: View) {
val context = view.context
context.startActivity(UpdatesActivity.newIntent(context))
}
private fun onListChanged(list: List<ListModel>) {
feedAdapter?.items = list

View File

@@ -5,21 +5,26 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.ui.model.DateTimeAgo
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.daysDiff
import org.koitharu.kotatsu.list.domain.ListExtraProvider
import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.list.ui.model.toUi
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.tracker.domain.model.TrackingLogItem
import org.koitharu.kotatsu.tracker.ui.feed.model.UpdatedMangaHeader
import org.koitharu.kotatsu.tracker.ui.feed.model.toFeedItem
import org.koitharu.kotatsu.tracker.work.TrackWorker
import java.util.Date
@@ -33,6 +38,7 @@ private const val PAGE_SIZE = 20
class FeedViewModel @Inject constructor(
private val repository: TrackingRepository,
private val scheduler: TrackWorker.Scheduler,
private val listExtraProvider: ListExtraProvider,
) : BaseViewModel() {
private val limit = MutableStateFlow(PAGE_SIZE)
@@ -42,22 +48,27 @@ class FeedViewModel @Inject constructor(
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, false)
val onFeedCleared = MutableEventFlow<Unit>()
val content = repository.observeTrackingLog(limit)
.map { list ->
if (list.isEmpty()) {
listOf(
EmptyState(
icon = R.drawable.ic_empty_feed,
textPrimary = R.string.text_empty_holder_primary,
textSecondary = R.string.text_feed_holder,
actionStringRes = 0,
),
)
} else {
isReady.set(true)
list.mapList()
}
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
val content = combine(
observeHeader(),
repository.observeTrackingLog(limit),
) { header, list ->
val result = ArrayList<ListModel>((list.size * 1.4).toInt().coerceAtLeast(2))
if (header != null) {
result += header
}
if (list.isEmpty()) {
result += EmptyState(
icon = R.drawable.ic_empty_feed,
textPrimary = R.string.text_empty_holder_primary,
textSecondary = R.string.text_feed_holder,
actionStringRes = 0,
)
} else {
isReady.set(true)
list.mapListTo(result)
}
result
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
init {
launchJob(Dispatchers.Default) {
@@ -85,8 +96,7 @@ class FeedViewModel @Inject constructor(
scheduler.startNow()
}
private fun List<TrackingLogItem>.mapList(): List<ListModel> {
val destination = ArrayList<ListModel>((size * 1.4).toInt())
private fun List<TrackingLogItem>.mapListTo(destination: MutableList<ListModel>) {
var prevDate: DateTimeAgo? = null
for (item in this) {
val date = timeAgo(item.createdAt)
@@ -96,7 +106,14 @@ class FeedViewModel @Inject constructor(
prevDate = date
destination += item.toFeedItem()
}
return destination
}
private fun observeHeader() = repository.observeUpdatedManga(10).map { mangaList ->
if (mangaList.isEmpty()) {
null
} else {
UpdatedMangaHeader(mangaList.toUi(ListMode.GRID, listExtraProvider))
}
}
private fun timeAgo(date: Date): DateTimeAgo {

View File

@@ -15,15 +15,27 @@ import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.size.ItemSizeResolver
class FeedAdapter(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
listener: MangaListListener,
sizeResolver: ItemSizeResolver,
) : BaseListAdapter<ListModel>(), FastScroller.SectionIndexer {
init {
addDelegate(ListItemType.FEED, feedItemAD(coil, lifecycleOwner, listener))
addDelegate(
ListItemType.MANGA_NESTED_GROUP,
updatedMangaAD(
lifecycleOwner = lifecycleOwner,
coil = coil,
sizeResolver = sizeResolver,
listener = listener,
headerClickListener = listener,
),
)
addDelegate(ListItemType.FOOTER_LOADING, loadingFooterAD())
addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
addDelegate(ListItemType.FOOTER_ERROR, errorFooterAD(listener))

View File

@@ -0,0 +1,44 @@
package org.koitharu.kotatsu.tracker.ui.feed.adapter
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.databinding.ItemListGroupBinding
import org.koitharu.kotatsu.list.ui.adapter.ListHeaderClickListener
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
import org.koitharu.kotatsu.list.ui.adapter.mangaGridItemAD
import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.size.ItemSizeResolver
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.tracker.ui.feed.model.UpdatedMangaHeader
fun updatedMangaAD(
lifecycleOwner: LifecycleOwner,
coil: ImageLoader,
sizeResolver: ItemSizeResolver,
listener: OnListItemClickListener<Manga>,
headerClickListener: ListHeaderClickListener,
) = adapterDelegateViewBinding<UpdatedMangaHeader, ListModel, ItemListGroupBinding>(
{ layoutInflater, parent -> ItemListGroupBinding.inflate(layoutInflater, parent, false) },
) {
val adapter = BaseListAdapter<ListModel>()
.addDelegate(ListItemType.MANGA_GRID, mangaGridItemAD(coil, lifecycleOwner, sizeResolver, listener))
binding.recyclerView.adapter = adapter
val spacing = context.resources.getDimensionPixelOffset(R.dimen.grid_spacing)
binding.recyclerView.addItemDecoration(SpacingItemDecoration(spacing))
binding.buttonMore.setOnClickListener { v ->
headerClickListener.onListHeaderClick(ListHeader(0, payload = item), v)
}
binding.textViewTitle.setText(R.string.updates)
binding.buttonMore.setText(R.string.more)
bind {
adapter.items = item.list
}
}

View File

@@ -0,0 +1,18 @@
package org.koitharu.kotatsu.tracker.ui.feed.model
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.MangaItemModel
data class UpdatedMangaHeader(
val list: List<MangaItemModel>,
) : ListModel {
override fun areItemsTheSame(other: ListModel): Boolean {
return other is UpdatedMangaHeader
}
override fun getChangePayload(previousState: ListModel): Any {
return ListModelDiffCallback.PAYLOAD_NESTED_LIST_CHANGED
}
}

View File

@@ -17,10 +17,7 @@
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"
android:paddingLeft="@dimen/list_spacing"
android:paddingTop="@dimen/grid_spacing_outer"
android:paddingRight="@dimen/list_spacing"
android:paddingBottom="@dimen/grid_spacing_outer"
app:bubbleSize="small"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_feed" />