Show updated manga on top of feed
This commit is contained in:
@@ -8,6 +8,7 @@ enum class ListItemType {
|
||||
MANGA_LIST,
|
||||
MANGA_LIST_DETAILED,
|
||||
MANGA_GRID,
|
||||
MANGA_NESTED_GROUP,
|
||||
FOOTER_LOADING,
|
||||
FOOTER_ERROR,
|
||||
STATE_LOADING,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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: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" />
|
||||
|
||||
|
||||
Reference in New Issue
Block a user