From 0271ed2ba974d21bea9f8e2866585e3baf08e7fe Mon Sep 17 00:00:00 2001 From: Koitharu Date: Fri, 11 Aug 2023 13:18:23 +0300 Subject: [PATCH] Show updated manga on top of feed --- .../kotatsu/list/ui/adapter/ListItemType.kt | 1 + .../ui/adapter/TypedListSpacingDecoration.kt | 3 +- .../ui/multi/adapter/SearchResultsAD.kt | 2 +- .../kotatsu/tracker/data/TracksDao.kt | 4 ++ .../tracker/domain/TrackingRepository.kt | 9 ++- .../kotatsu/tracker/ui/feed/FeedFragment.kt | 10 +++- .../kotatsu/tracker/ui/feed/FeedViewModel.kt | 55 ++++++++++++------- .../tracker/ui/feed/adapter/FeedAdapter.kt | 12 ++++ .../tracker/ui/feed/adapter/UpdatedMangaAD.kt | 44 +++++++++++++++ .../ui/feed/model/UpdatedMangaHeader.kt | 18 ++++++ app/src/main/res/layout/fragment_feed.xml | 5 +- 11 files changed, 133 insertions(+), 30 deletions(-) create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/adapter/UpdatedMangaAD.kt create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/model/UpdatedMangaHeader.kt diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ListItemType.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ListItemType.kt index 270e09467..b12eb169f 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ListItemType.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ListItemType.kt @@ -8,6 +8,7 @@ enum class ListItemType { MANGA_LIST, MANGA_LIST_DETAILED, MANGA_GRID, + MANGA_NESTED_GROUP, FOOTER_LOADING, FOOTER_ERROR, STATE_LOADING, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/TypedListSpacingDecoration.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/TypedListSpacingDecoration.kt index 8a9db6002..a540d43d8 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/TypedListSpacingDecoration.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/TypedListSpacingDecoration.kt @@ -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) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/adapter/SearchResultsAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/adapter/SearchResultsAD.kt index d2cc89e47..6f945677f 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/adapter/SearchResultsAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/adapter/SearchResultsAD.kt @@ -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) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/TracksDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/TracksDao.kt index c2467b94b..357a98c57 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/TracksDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/TracksDao.kt @@ -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> + @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> + @Query("DELETE FROM tracks") abstract suspend fun clear() diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt index b410546d3..8f4eff796 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt @@ -49,9 +49,12 @@ class TrackingRepository @Inject constructor( .onStart { gcIfNotCalled() } } - fun observeUpdatedManga(): Flow> { - return db.tracksDao.observeUpdatedManga() - .mapItems { it.toManga() } + fun observeUpdatedManga(limit: Int = 0): Flow> { + return if (limit == 0) { + db.tracksDao.observeUpdatedManga() + } else { + db.tracksDao.observeUpdatedManga(limit) + }.mapItems { it.toManga() } .distinctUntilChanged() .onStart { gcIfNotCalled() } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedFragment.kt index 83a0a10cd..faa8594c4 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedFragment.kt @@ -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) { feedAdapter?.items = list diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedViewModel.kt index 5c30e4d1d..7550fad36 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedViewModel.kt @@ -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() - 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((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.mapList(): List { - val destination = ArrayList((size * 1.4).toInt()) + private fun List.mapListTo(destination: MutableList) { 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 { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/adapter/FeedAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/adapter/FeedAdapter.kt index e7a8530bb..e39f5055d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/adapter/FeedAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/adapter/FeedAdapter.kt @@ -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(), 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)) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/adapter/UpdatedMangaAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/adapter/UpdatedMangaAD.kt new file mode 100644 index 000000000..13c59d044 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/adapter/UpdatedMangaAD.kt @@ -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, + headerClickListener: ListHeaderClickListener, +) = adapterDelegateViewBinding( + { layoutInflater, parent -> ItemListGroupBinding.inflate(layoutInflater, parent, false) }, +) { + + val adapter = BaseListAdapter() + .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 + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/model/UpdatedMangaHeader.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/model/UpdatedMangaHeader.kt new file mode 100644 index 000000000..71e2ce8de --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/model/UpdatedMangaHeader.kt @@ -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, +) : ListModel { + + override fun areItemsTheSame(other: ListModel): Boolean { + return other is UpdatedMangaHeader + } + + override fun getChangePayload(previousState: ListModel): Any { + return ListModelDiffCallback.PAYLOAD_NESTED_LIST_CHANGED + } +} diff --git a/app/src/main/res/layout/fragment_feed.xml b/app/src/main/res/layout/fragment_feed.xml index dba209dca..0eb8c87b9 100644 --- a/app/src/main/res/layout/fragment_feed.xml +++ b/app/src/main/res/layout/fragment_feed.xml @@ -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" />