Show updated manga on top of feed

pull/450/head
Koitharu 3 years ago
parent 788c7b862a
commit 0271ed2ba9
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

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

@ -44,11 +44,12 @@ class TypedListSpacingDecoration(
ListItemType.EXPLORE_SOURCE_GRID, ListItemType.EXPLORE_SOURCE_GRID,
ListItemType.EXPLORE_SOURCE_LIST, ListItemType.EXPLORE_SOURCE_LIST,
ListItemType.EXPLORE_SUGGESTION, ListItemType.EXPLORE_SUGGESTION,
ListItemType.MANGA_NESTED_GROUP,
null -> outRect.set(0) null -> outRect.set(0)
ListItemType.TIP -> outRect.set(0) // TODO ListItemType.TIP -> outRect.set(0) // TODO
ListItemType.HINT_EMPTY -> 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)
} }
} }

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

@ -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") @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>> 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") @Query("DELETE FROM tracks")
abstract suspend fun clear() abstract suspend fun clear()

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

@ -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.adapter.TypedListSpacingDecoration
import org.koitharu.kotatsu.list.ui.model.ListHeader import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel 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.main.ui.owners.BottomNavOwner
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.tracker.ui.feed.adapter.FeedAdapter import org.koitharu.kotatsu.tracker.ui.feed.adapter.FeedAdapter
import org.koitharu.kotatsu.tracker.ui.updates.UpdatesActivity
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -50,7 +52,8 @@ class FeedFragment :
override fun onViewBindingCreated(binding: FragmentFeedBinding, savedInstanceState: Bundle?) { override fun onViewBindingCreated(binding: FragmentFeedBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState) 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) { with(binding.recyclerView) {
adapter = feedAdapter adapter = feedAdapter
setHasFixedSize(true) setHasFixedSize(true)
@ -96,7 +99,10 @@ class FeedFragment :
override fun onEmptyActionClick() = Unit 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>) { private fun onListChanged(list: List<ListModel>) {
feedAdapter?.items = list feedAdapter?.items = list

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

@ -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.adapter.loadingStateAD
import org.koitharu.kotatsu.list.ui.model.ListHeader import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.size.ItemSizeResolver
class FeedAdapter( class FeedAdapter(
coil: ImageLoader, coil: ImageLoader,
lifecycleOwner: LifecycleOwner, lifecycleOwner: LifecycleOwner,
listener: MangaListListener, listener: MangaListListener,
sizeResolver: ItemSizeResolver,
) : BaseListAdapter<ListModel>(), FastScroller.SectionIndexer { ) : BaseListAdapter<ListModel>(), FastScroller.SectionIndexer {
init { init {
addDelegate(ListItemType.FEED, feedItemAD(coil, lifecycleOwner, listener)) 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.FOOTER_LOADING, loadingFooterAD())
addDelegate(ListItemType.STATE_LOADING, loadingStateAD()) addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
addDelegate(ListItemType.FOOTER_ERROR, errorFooterAD(listener)) addDelegate(ListItemType.FOOTER_ERROR, errorFooterAD(listener))

@ -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
}
}

@ -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
}
}

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

Loading…
Cancel
Save