Migrate feed to adapter delegates
This commit is contained in:
@@ -1,34 +1,18 @@
|
|||||||
package org.koitharu.kotatsu.base.ui.list
|
package org.koitharu.kotatsu.base.ui.list
|
||||||
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
|
||||||
class PaginationScrollListener(offset: Int, private val callback: Callback) :
|
class PaginationScrollListener(offset: Int, private val callback: Callback) :
|
||||||
BoundsScrollListener(0, offset) {
|
BoundsScrollListener(0, offset) {
|
||||||
|
|
||||||
private var lastTotalCount = 0
|
|
||||||
|
|
||||||
override fun onScrolledToStart(recyclerView: RecyclerView) = Unit
|
override fun onScrolledToStart(recyclerView: RecyclerView) = Unit
|
||||||
|
|
||||||
override fun onScrolledToEnd(recyclerView: RecyclerView) {
|
override fun onScrolledToEnd(recyclerView: RecyclerView) {
|
||||||
val total = (recyclerView.layoutManager as? LinearLayoutManager)?.itemCount ?: return
|
callback.onScrolledToEnd()
|
||||||
if (total > lastTotalCount) {
|
|
||||||
lastTotalCount = total
|
|
||||||
callback.onRequestMoreItems(total)
|
|
||||||
} else if (total < lastTotalCount) {
|
|
||||||
lastTotalCount = total
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun reset() {
|
|
||||||
lastTotalCount = 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Callback {
|
interface Callback {
|
||||||
|
|
||||||
fun onRequestMoreItems(offset: Int)
|
fun onScrolledToEnd()
|
||||||
|
|
||||||
@Deprecated("Not in use")
|
|
||||||
fun getItemsCount(): Int = 0
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -20,7 +20,9 @@ class FavouritesListFragment : MangaListFragment() {
|
|||||||
private val categoryId: Long
|
private val categoryId: Long
|
||||||
get() = arguments?.getLong(ARG_CATEGORY_ID) ?: 0L
|
get() = arguments?.getLong(ARG_CATEGORY_ID) ?: 0L
|
||||||
|
|
||||||
override fun onRequestMoreItems(offset: Int) = Unit
|
override val isSwipeRefreshEnabled = false
|
||||||
|
|
||||||
|
override fun onScrolledToEnd() = Unit
|
||||||
|
|
||||||
override fun setUpEmptyListHolder() {
|
override fun setUpEmptyListHolder() {
|
||||||
textView_holder.setText(
|
textView_holder.setText(
|
||||||
|
|||||||
@@ -17,17 +17,14 @@ import org.koitharu.kotatsu.utils.ext.ellipsize
|
|||||||
class HistoryListFragment : MangaListFragment() {
|
class HistoryListFragment : MangaListFragment() {
|
||||||
|
|
||||||
override val viewModel by viewModel<HistoryListViewModel>()
|
override val viewModel by viewModel<HistoryListViewModel>()
|
||||||
|
override val isSwipeRefreshEnabled = false
|
||||||
init {
|
|
||||||
isSwipeRefreshEnabled = false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
viewModel.onItemRemoved.observe(viewLifecycleOwner, ::onItemRemoved)
|
viewModel.onItemRemoved.observe(viewLifecycleOwner, ::onItemRemoved)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRequestMoreItems(offset: Int) = Unit
|
override fun onScrolledToEnd() = Unit
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
inflater.inflate(R.menu.opt_history, menu)
|
inflater.inflate(R.menu.opt_history, menu)
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list),
|
|||||||
private var paginationListener: PaginationScrollListener? = null
|
private var paginationListener: PaginationScrollListener? = null
|
||||||
private val spanResolver = MangaListSpanResolver()
|
private val spanResolver = MangaListSpanResolver()
|
||||||
private val spanSizeLookup = SpanSizeLookup()
|
private val spanSizeLookup = SpanSizeLookup()
|
||||||
protected var isSwipeRefreshEnabled = true
|
open val isSwipeRefreshEnabled = true
|
||||||
|
|
||||||
protected abstract val viewModel: MangaListViewModel
|
protected abstract val viewModel: MangaListViewModel
|
||||||
|
|
||||||
@@ -63,6 +63,7 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list),
|
|||||||
recyclerView.adapter = adapter
|
recyclerView.adapter = adapter
|
||||||
recyclerView.addOnScrollListener(paginationListener!!)
|
recyclerView.addOnScrollListener(paginationListener!!)
|
||||||
swipeRefreshLayout.setOnRefreshListener(this)
|
swipeRefreshLayout.setOnRefreshListener(this)
|
||||||
|
swipeRefreshLayout.isEnabled = isSwipeRefreshEnabled
|
||||||
recyclerView_filter.setHasFixedSize(true)
|
recyclerView_filter.setHasFixedSize(true)
|
||||||
recyclerView_filter.addItemDecoration(ItemTypeDividerDecoration(view.context))
|
recyclerView_filter.addItemDecoration(ItemTypeDividerDecoration(view.context))
|
||||||
recyclerView_filter.addItemDecoration(SectionItemDecoration(false, this))
|
recyclerView_filter.addItemDecoration(SectionItemDecoration(false, this))
|
||||||
@@ -125,9 +126,9 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final override fun onRefresh() {
|
@CallSuper
|
||||||
|
override fun onRefresh() {
|
||||||
swipeRefreshLayout.isRefreshing = true
|
swipeRefreshLayout.isRefreshing = true
|
||||||
onRequestMoreItems(0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onListChanged(list: List<Any>) {
|
private fun onListChanged(list: List<Any>) {
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ abstract class MangaListSheet : BaseBottomSheet(R.layout.sheet_list),
|
|||||||
appbar.elevation = resources.getDimension(R.dimen.elevation_large)
|
appbar.elevation = resources.getDimension(R.dimen.elevation_large)
|
||||||
}
|
}
|
||||||
if (savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
onRequestMoreItems(0)
|
onScrolledToEnd()
|
||||||
}
|
}
|
||||||
viewModel.content.observe(viewLifecycleOwner, ::onListChanged)
|
viewModel.content.observe(viewLifecycleOwner, ::onListChanged)
|
||||||
viewModel.onError.observe(viewLifecycleOwner, ::onError)
|
viewModel.onError.observe(viewLifecycleOwner, ::onError)
|
||||||
@@ -128,8 +128,6 @@ abstract class MangaListSheet : BaseBottomSheet(R.layout.sheet_list),
|
|||||||
recyclerView.callOnScrollListeners()
|
recyclerView.callOnScrollListeners()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemsCount() = adapter?.itemCount ?: 0
|
|
||||||
|
|
||||||
private fun onError(e: Throwable) {
|
private fun onError(e: Throwable) {
|
||||||
Snackbar.make(recyclerView, e.getDisplayMessage(resources), Snackbar.LENGTH_SHORT).show()
|
Snackbar.make(recyclerView, e.getDisplayMessage(resources), Snackbar.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
@@ -151,7 +149,7 @@ abstract class MangaListSheet : BaseBottomSheet(R.layout.sheet_list),
|
|||||||
ListMode.GRID -> {
|
ListMode.GRID -> {
|
||||||
GridLayoutManager(ctx, UiUtils.resolveGridSpanCount(ctx)).apply {
|
GridLayoutManager(ctx, UiUtils.resolveGridSpanCount(ctx)).apply {
|
||||||
spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
||||||
override fun getSpanSize(position: Int) = if (position < getItemsCount())
|
override fun getSpanSize(position: Int) = if (position < TODO() as Int)
|
||||||
1 else this@apply.spanCount
|
1 else this@apply.spanCount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,12 +32,13 @@ class LocalListFragment : MangaListFragment(), ActivityResultCallback<Uri> {
|
|||||||
viewModel.onMangaRemoved.observe(viewLifecycleOwner, ::onItemRemoved)
|
viewModel.onMangaRemoved.observe(viewLifecycleOwner, ::onItemRemoved)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRequestMoreItems(offset: Int) {
|
override fun onRefresh() {
|
||||||
if (offset == 0) {
|
super.onRefresh()
|
||||||
viewModel.onRefresh()
|
viewModel.onRefresh()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onScrolledToEnd() = Unit
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
inflater.inflate(R.menu.opt_local, menu)
|
inflater.inflate(R.menu.opt_local, menu)
|
||||||
super.onCreateOptionsMenu(menu, inflater)
|
super.onCreateOptionsMenu(menu, inflater)
|
||||||
|
|||||||
@@ -45,11 +45,17 @@ class LocalListViewModel(
|
|||||||
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
loadList()
|
onRefresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onRefresh() {
|
fun onRefresh() {
|
||||||
loadList()
|
launchLoadingJob {
|
||||||
|
withContext(Dispatchers.Default) {
|
||||||
|
val list = repository.getList(0)
|
||||||
|
mangaList.value = list
|
||||||
|
isEmptyState.postValue(list.isEmpty())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun importFile(uri: Uri) {
|
fun importFile(uri: Uri) {
|
||||||
@@ -69,7 +75,7 @@ class LocalListViewModel(
|
|||||||
}
|
}
|
||||||
} ?: throw IOException("Cannot open input stream: $uri")
|
} ?: throw IOException("Cannot open input stream: $uri")
|
||||||
}
|
}
|
||||||
loadList()
|
onRefresh()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,14 +94,4 @@ class LocalListViewModel(
|
|||||||
onMangaRemoved.call(manga)
|
onMangaRemoved.call(manga)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadList() {
|
|
||||||
launchLoadingJob {
|
|
||||||
withContext(Dispatchers.Default) {
|
|
||||||
val list = repository.getList(0)
|
|
||||||
mangaList.value = list
|
|
||||||
isEmptyState.postValue(list.isEmpty())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -21,8 +21,13 @@ class RemoteListFragment : MangaListFragment() {
|
|||||||
|
|
||||||
private val source by parcelableArgument<MangaSource>(ARG_SOURCE)
|
private val source by parcelableArgument<MangaSource>(ARG_SOURCE)
|
||||||
|
|
||||||
override fun onRequestMoreItems(offset: Int) {
|
override fun onRefresh() {
|
||||||
viewModel.loadList(offset)
|
super.onRefresh()
|
||||||
|
viewModel.loadList(append = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onScrolledToEnd() {
|
||||||
|
viewModel.loadList(append = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getTitle(): CharSequence? {
|
override fun getTitle(): CharSequence? {
|
||||||
|
|||||||
@@ -45,22 +45,22 @@ class RemoteListViewModel(
|
|||||||
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
loadList(0)
|
loadList(false)
|
||||||
loadFilter()
|
loadFilter()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadList(offset: Int) {
|
fun loadList(append: Boolean) {
|
||||||
if (loadingJob?.isActive == true) {
|
if (loadingJob?.isActive == true) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
loadingJob = launchLoadingJob {
|
loadingJob = launchLoadingJob {
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
val list = repository.getList(
|
val list = repository.getList(
|
||||||
offset = offset,
|
offset = if (append) mangaList.value.size else 0,
|
||||||
sortOrder = appliedFilter?.sortOrder,
|
sortOrder = appliedFilter?.sortOrder,
|
||||||
tag = appliedFilter?.tag
|
tag = appliedFilter?.tag
|
||||||
)
|
)
|
||||||
if (offset == 0) {
|
if (!append) {
|
||||||
mangaList.value = list
|
mangaList.value = list
|
||||||
} else if (list.isNotEmpty()) {
|
} else if (list.isNotEmpty()) {
|
||||||
mangaList.value += list
|
mangaList.value += list
|
||||||
@@ -74,7 +74,7 @@ class RemoteListViewModel(
|
|||||||
appliedFilter = newFilter
|
appliedFilter = newFilter
|
||||||
mangaList.value = emptyList()
|
mangaList.value = emptyList()
|
||||||
hasNextPage.value = false
|
hasNextPage.value = false
|
||||||
loadList(0)
|
loadList(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadFilter() {
|
private fun loadFilter() {
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ class MangaSearchSheet : MangaListSheet() {
|
|||||||
setSubtitle(getString(R.string.search_results_on_s, source.title))
|
setSubtitle(getString(R.string.search_results_on_s, source.title))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRequestMoreItems(offset: Int) {
|
override fun onScrolledToEnd() {
|
||||||
viewModel.loadList(query.orEmpty(), offset)
|
viewModel.loadList(query.orEmpty(), append = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -17,8 +17,13 @@ class SearchFragment : MangaListFragment() {
|
|||||||
private val query by stringArgument(ARG_QUERY)
|
private val query by stringArgument(ARG_QUERY)
|
||||||
private val source by parcelableArgument<MangaSource>(ARG_SOURCE)
|
private val source by parcelableArgument<MangaSource>(ARG_SOURCE)
|
||||||
|
|
||||||
override fun onRequestMoreItems(offset: Int) {
|
override fun onRefresh() {
|
||||||
viewModel.loadList(query.orEmpty(), offset)
|
super.onRefresh()
|
||||||
|
viewModel.loadList(query.orEmpty(), append = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onScrolledToEnd() {
|
||||||
|
viewModel.loadList(query.orEmpty(), append = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getTitle(): CharSequence? {
|
override fun getTitle(): CharSequence? {
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ class SearchViewModel(
|
|||||||
|
|
||||||
override val content = MutableLiveData<List<Any>>()
|
override val content = MutableLiveData<List<Any>>()
|
||||||
|
|
||||||
fun loadList(query: String, offset: Int) {
|
fun loadList(query: String, append: Boolean) {
|
||||||
launchLoadingJob {
|
launchLoadingJob {
|
||||||
val list = withContext(Dispatchers.Default) {
|
val list = withContext(Dispatchers.Default) {
|
||||||
repository.getList(offset, query = query)
|
repository.getList(TODO(), query = query)
|
||||||
}
|
}
|
||||||
if (offset == 0) {
|
if (!append) {
|
||||||
content.value = list
|
content.value = list
|
||||||
} else {
|
} else {
|
||||||
content.value = content.value.orEmpty() + list
|
content.value = content.value.orEmpty() + list
|
||||||
|
|||||||
@@ -12,12 +12,13 @@ class GlobalSearchFragment : MangaListFragment() {
|
|||||||
|
|
||||||
private val query by stringArgument(ARG_QUERY)
|
private val query by stringArgument(ARG_QUERY)
|
||||||
|
|
||||||
override fun onRequestMoreItems(offset: Int) {
|
override fun onRefresh() {
|
||||||
if (offset == 0) {
|
super.onRefresh()
|
||||||
viewModel.startSearch(query.orEmpty())
|
viewModel.startSearch(query.orEmpty())
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onScrolledToEnd() = Unit
|
||||||
|
|
||||||
override fun getTitle(): CharSequence? {
|
override fun getTitle(): CharSequence? {
|
||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.koitharu.kotatsu.tracker
|
package org.koitharu.kotatsu.tracker
|
||||||
|
|
||||||
|
import org.koin.android.ext.koin.androidContext
|
||||||
import org.koin.android.viewmodel.dsl.viewModel
|
import org.koin.android.viewmodel.dsl.viewModel
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||||
@@ -10,5 +11,5 @@ val trackerModule
|
|||||||
|
|
||||||
single { TrackingRepository(get(), get()) }
|
single { TrackingRepository(get(), get()) }
|
||||||
|
|
||||||
viewModel { FeedViewModel(get()) }
|
viewModel { FeedViewModel(androidContext(), get()) }
|
||||||
}
|
}
|
||||||
@@ -1,19 +1,46 @@
|
|||||||
package org.koitharu.kotatsu.tracker.ui
|
package org.koitharu.kotatsu.tracker.ui
|
||||||
|
|
||||||
import android.view.ViewGroup
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import org.koitharu.kotatsu.base.ui.list.BaseRecyclerAdapter
|
import coil.ImageLoader
|
||||||
import org.koitharu.kotatsu.base.ui.list.BaseViewHolder
|
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnRecyclerItemClickListener
|
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||||
import org.koitharu.kotatsu.core.model.TrackingLogItem
|
import org.koitharu.kotatsu.core.model.Manga
|
||||||
|
import org.koitharu.kotatsu.list.ui.adapter.indeterminateProgressAD
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.IndeterminateProgress
|
||||||
|
import org.koitharu.kotatsu.tracker.ui.adapter.feedItemAD
|
||||||
|
import org.koitharu.kotatsu.tracker.ui.model.FeedItem
|
||||||
|
import kotlin.jvm.internal.Intrinsics
|
||||||
|
|
||||||
class FeedAdapter(onItemClickListener: OnRecyclerItemClickListener<TrackingLogItem>? = null) :
|
class FeedAdapter(
|
||||||
BaseRecyclerAdapter<TrackingLogItem, Unit>(onItemClickListener) {
|
coil: ImageLoader,
|
||||||
|
clickListener: OnListItemClickListener<Manga>
|
||||||
|
) : AsyncListDifferDelegationAdapter<Any>(DiffCallback()) {
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup): BaseViewHolder<TrackingLogItem, Unit> {
|
init {
|
||||||
return FeedHolder(parent)
|
delegatesManager.addDelegate(ITEM_TYPE_FEED, feedItemAD(coil, clickListener))
|
||||||
|
.addDelegate(ITEM_TYPE_PROGRESS, indeterminateProgressAD())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onGetItemId(item: TrackingLogItem) = item.id
|
private class DiffCallback : DiffUtil.ItemCallback<Any>() {
|
||||||
|
|
||||||
override fun getExtra(item: TrackingLogItem, position: Int) = Unit
|
override fun areItemsTheSame(oldItem: Any, newItem: Any) = when {
|
||||||
|
oldItem is FeedItem && newItem is FeedItem -> {
|
||||||
|
oldItem.id == newItem.id
|
||||||
|
}
|
||||||
|
oldItem == IndeterminateProgress && newItem == IndeterminateProgress -> {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: Any, newItem: Any): Boolean {
|
||||||
|
return Intrinsics.areEqual(oldItem, newItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val ITEM_TYPE_FEED = 0
|
||||||
|
const val ITEM_TYPE_PROGRESS = 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -8,21 +8,21 @@ import android.view.View
|
|||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import kotlinx.android.synthetic.main.fragment_tracklogs.*
|
import kotlinx.android.synthetic.main.fragment_tracklogs.*
|
||||||
|
import org.koin.android.ext.android.get
|
||||||
import org.koin.android.viewmodel.ext.android.viewModel
|
import org.koin.android.viewmodel.ext.android.viewModel
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.ui.BaseFragment
|
import org.koitharu.kotatsu.base.ui.BaseFragment
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnRecyclerItemClickListener
|
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||||
import org.koitharu.kotatsu.base.ui.list.PaginationScrollListener
|
import org.koitharu.kotatsu.base.ui.list.PaginationScrollListener
|
||||||
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
|
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
|
||||||
import org.koitharu.kotatsu.core.model.TrackingLogItem
|
import org.koitharu.kotatsu.core.model.Manga
|
||||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||||
import org.koitharu.kotatsu.tracker.work.TrackWorker
|
import org.koitharu.kotatsu.tracker.work.TrackWorker
|
||||||
import org.koitharu.kotatsu.utils.ext.callOnScrollListeners
|
|
||||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||||
import org.koitharu.kotatsu.utils.ext.hasItems
|
import org.koitharu.kotatsu.utils.ext.hasItems
|
||||||
|
|
||||||
class FeedFragment : BaseFragment(R.layout.fragment_tracklogs), PaginationScrollListener.Callback,
|
class FeedFragment : BaseFragment(R.layout.fragment_tracklogs), PaginationScrollListener.Callback,
|
||||||
OnRecyclerItemClickListener<TrackingLogItem> {
|
OnListItemClickListener<Manga> {
|
||||||
|
|
||||||
private val viewModel by viewModel<FeedViewModel>()
|
private val viewModel by viewModel<FeedViewModel>()
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ class FeedFragment : BaseFragment(R.layout.fragment_tracklogs), PaginationScroll
|
|||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
adapter = FeedAdapter(this)
|
adapter = FeedAdapter(get(), this)
|
||||||
recyclerView.adapter = adapter
|
recyclerView.adapter = adapter
|
||||||
recyclerView.addItemDecoration(
|
recyclerView.addItemDecoration(
|
||||||
SpacingItemDecoration(resources.getDimensionPixelOffset(R.dimen.grid_spacing))
|
SpacingItemDecoration(resources.getDimensionPixelOffset(R.dimen.grid_spacing))
|
||||||
@@ -45,12 +45,13 @@ class FeedFragment : BaseFragment(R.layout.fragment_tracklogs), PaginationScroll
|
|||||||
recyclerView.setHasFixedSize(true)
|
recyclerView.setHasFixedSize(true)
|
||||||
recyclerView.addOnScrollListener(PaginationScrollListener(4, this))
|
recyclerView.addOnScrollListener(PaginationScrollListener(4, this))
|
||||||
if (savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
onRequestMoreItems(0)
|
onScrolledToEnd()
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.content.observe(viewLifecycleOwner, this::onListChanged)
|
viewModel.content.observe(viewLifecycleOwner, this::onListChanged)
|
||||||
viewModel.isLoading.observe(viewLifecycleOwner, this::onLoadingStateChanged)
|
viewModel.isLoading.observe(viewLifecycleOwner, this::onLoadingStateChanged)
|
||||||
viewModel.onError.observe(viewLifecycleOwner, this::onError)
|
viewModel.onError.observe(viewLifecycleOwner, this::onError)
|
||||||
|
viewModel.isEmptyState.observe(viewLifecycleOwner, this::onEmptyStateChanged)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
@@ -72,15 +73,8 @@ class FeedFragment : BaseFragment(R.layout.fragment_tracklogs), PaginationScroll
|
|||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onListChanged(list: List<TrackingLogItem>) {
|
private fun onListChanged(list: List<Any>) {
|
||||||
adapter?.replaceData(list)
|
adapter?.items = list
|
||||||
if (list.isEmpty()) {
|
|
||||||
setUpEmptyListHolder()
|
|
||||||
layout_holder.isVisible = true
|
|
||||||
} else {
|
|
||||||
layout_holder.isVisible = false
|
|
||||||
}
|
|
||||||
recyclerView.callOnScrollListeners()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onError(e: Throwable) {
|
private fun onError(e: Throwable) {
|
||||||
@@ -102,21 +96,21 @@ class FeedFragment : BaseFragment(R.layout.fragment_tracklogs), PaginationScroll
|
|||||||
private fun onLoadingStateChanged(isLoading: Boolean) {
|
private fun onLoadingStateChanged(isLoading: Boolean) {
|
||||||
val hasItems = recyclerView.hasItems
|
val hasItems = recyclerView.hasItems
|
||||||
progressBar.isVisible = isLoading && !hasItems
|
progressBar.isVisible = isLoading && !hasItems
|
||||||
if (isLoading) {
|
}
|
||||||
layout_holder.isVisible = false
|
|
||||||
|
private fun onEmptyStateChanged(isEmpty: Boolean) {
|
||||||
|
if (isEmpty) {
|
||||||
|
setUpEmptyListHolder()
|
||||||
}
|
}
|
||||||
|
layout_holder.isVisible = isEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemsCount(): Int {
|
override fun onScrolledToEnd() {
|
||||||
return adapter?.itemCount ?: 0
|
viewModel.loadList(append = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRequestMoreItems(offset: Int) {
|
override fun onItemClick(item: Manga, view: View) {
|
||||||
viewModel.loadList(offset)
|
startActivity(DetailsActivity.newIntent(context ?: return, item))
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemClick(item: TrackingLogItem, position: Int, view: View) {
|
|
||||||
startActivity(DetailsActivity.newIntent(context ?: return, item.manga))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setUpEmptyListHolder() {
|
private fun setUpEmptyListHolder() {
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.tracker.ui
|
|
||||||
|
|
||||||
import android.text.format.DateUtils
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import coil.ImageLoader
|
|
||||||
import coil.request.Disposable
|
|
||||||
import kotlinx.android.synthetic.main.item_tracklog.*
|
|
||||||
import org.koin.core.component.inject
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.base.ui.list.BaseViewHolder
|
|
||||||
import org.koitharu.kotatsu.core.model.TrackingLogItem
|
|
||||||
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
|
||||||
import org.koitharu.kotatsu.utils.ext.formatRelative
|
|
||||||
import org.koitharu.kotatsu.utils.ext.newImageRequest
|
|
||||||
|
|
||||||
class FeedHolder(parent: ViewGroup) :
|
|
||||||
BaseViewHolder<TrackingLogItem, Unit>(parent, R.layout.item_tracklog) {
|
|
||||||
|
|
||||||
private val coil by inject<ImageLoader>()
|
|
||||||
private var imageRequest: Disposable? = null
|
|
||||||
|
|
||||||
override fun onBind(data: TrackingLogItem, extra: Unit) {
|
|
||||||
imageRequest?.dispose()
|
|
||||||
imageRequest = imageView_cover.newImageRequest(data.manga.coverUrl)
|
|
||||||
.placeholder(R.drawable.ic_placeholder)
|
|
||||||
.fallback(R.drawable.ic_placeholder)
|
|
||||||
.error(R.drawable.ic_placeholder)
|
|
||||||
.enqueueWith(coil)
|
|
||||||
textView_title.text = data.manga.title
|
|
||||||
textView_subtitle.text = buildString {
|
|
||||||
append(data.createdAt.formatRelative(DateUtils.DAY_IN_MILLIS))
|
|
||||||
append(" ")
|
|
||||||
append(
|
|
||||||
context.resources.getQuantityString(
|
|
||||||
R.plurals.new_chapters,
|
|
||||||
data.chapters.size,
|
|
||||||
data.chapters.size
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
textView_chapters.text = data.chapters.joinToString("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRecycled() {
|
|
||||||
imageRequest?.dispose()
|
|
||||||
imageView_cover.setImageDrawable(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +1,60 @@
|
|||||||
package org.koitharu.kotatsu.tracker.ui
|
package org.koitharu.kotatsu.tracker.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.asLiveData
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.drop
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||||
import org.koitharu.kotatsu.core.model.TrackingLogItem
|
import org.koitharu.kotatsu.core.model.TrackingLogItem
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.IndeterminateProgress
|
||||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||||
|
import org.koitharu.kotatsu.tracker.ui.model.toFeedItem
|
||||||
|
import org.koitharu.kotatsu.utils.ext.mapItems
|
||||||
|
|
||||||
class FeedViewModel(
|
class FeedViewModel(
|
||||||
|
context: Context,
|
||||||
private val repository: TrackingRepository
|
private val repository: TrackingRepository
|
||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
val content = MutableLiveData<List<TrackingLogItem>>()
|
private val logList = MutableStateFlow<List<TrackingLogItem>>(emptyList())
|
||||||
|
private val hasNextPage = MutableStateFlow(false)
|
||||||
|
private var loadingJob: Job? = null
|
||||||
|
|
||||||
fun loadList(offset: Int) {
|
val isEmptyState = MutableLiveData(false)
|
||||||
launchLoadingJob {
|
val content = combine(
|
||||||
|
logList.drop(1).onEach {
|
||||||
|
isEmptyState.postValue(it.isEmpty())
|
||||||
|
}.mapItems {
|
||||||
|
it.toFeedItem(context.resources)
|
||||||
|
},
|
||||||
|
hasNextPage
|
||||||
|
) { list, isHasNextPage ->
|
||||||
|
if (isHasNextPage && list.isNotEmpty()) list + IndeterminateProgress else list
|
||||||
|
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||||
|
|
||||||
|
init {
|
||||||
|
loadList(append = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadList(append: Boolean) {
|
||||||
|
if (loadingJob?.isActive == true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loadingJob = launchLoadingJob {
|
||||||
|
val offset = if (append) logList.value.size else 0
|
||||||
val list = repository.getTrackingLog(offset, 20)
|
val list = repository.getTrackingLog(offset, 20)
|
||||||
if (offset == 0) {
|
if (!append) {
|
||||||
content.value = list
|
logList.value = list
|
||||||
} else {
|
} else if (list.isNotEmpty()) {
|
||||||
content.value = content.value.orEmpty() + list
|
logList.value += list
|
||||||
}
|
}
|
||||||
|
hasNextPage.value = list.isNotEmpty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package org.koitharu.kotatsu.tracker.ui.adapter
|
||||||
|
|
||||||
|
import coil.ImageLoader
|
||||||
|
import coil.request.Disposable
|
||||||
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateLayoutContainer
|
||||||
|
import kotlinx.android.synthetic.main.item_tracklog.*
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||||
|
import org.koitharu.kotatsu.core.model.Manga
|
||||||
|
import org.koitharu.kotatsu.tracker.ui.model.FeedItem
|
||||||
|
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||||
|
import org.koitharu.kotatsu.utils.ext.newImageRequest
|
||||||
|
|
||||||
|
fun feedItemAD(
|
||||||
|
coil: ImageLoader,
|
||||||
|
clickListener: OnListItemClickListener<Manga>
|
||||||
|
) = adapterDelegateLayoutContainer<FeedItem, Any>(R.layout.item_tracklog) {
|
||||||
|
|
||||||
|
var imageRequest: Disposable? = null
|
||||||
|
|
||||||
|
itemView.setOnClickListener {
|
||||||
|
clickListener.onItemClick(item.manga, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
bind {
|
||||||
|
imageRequest?.dispose()
|
||||||
|
imageRequest = imageView_cover.newImageRequest(item.imageUrl)
|
||||||
|
.placeholder(R.drawable.ic_placeholder)
|
||||||
|
.fallback(R.drawable.ic_placeholder)
|
||||||
|
.error(R.drawable.ic_placeholder)
|
||||||
|
.enqueueWith(coil)
|
||||||
|
textView_title.text = item.title
|
||||||
|
textView_subtitle.text = item.subtitle
|
||||||
|
textView_chapters.text = item.chapters
|
||||||
|
}
|
||||||
|
|
||||||
|
onViewRecycled {
|
||||||
|
imageRequest?.dispose()
|
||||||
|
imageView_cover.setImageDrawable(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package org.koitharu.kotatsu.tracker.ui.model
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.core.model.Manga
|
||||||
|
|
||||||
|
data class FeedItem(
|
||||||
|
val id: Long,
|
||||||
|
val imageUrl: String,
|
||||||
|
val title: String,
|
||||||
|
val subtitle: String,
|
||||||
|
val chapters: CharSequence,
|
||||||
|
val manga: Manga
|
||||||
|
)
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package org.koitharu.kotatsu.tracker.ui.model
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
|
import android.text.format.DateUtils
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.model.TrackingLogItem
|
||||||
|
import org.koitharu.kotatsu.utils.ext.formatRelative
|
||||||
|
|
||||||
|
fun TrackingLogItem.toFeedItem(resources: Resources) = FeedItem(
|
||||||
|
id = id,
|
||||||
|
imageUrl = manga.coverUrl,
|
||||||
|
title = manga.title,
|
||||||
|
subtitle = buildString {
|
||||||
|
append(createdAt.formatRelative(DateUtils.DAY_IN_MILLIS))
|
||||||
|
append(" ")
|
||||||
|
append(
|
||||||
|
resources.getQuantityString(
|
||||||
|
R.plurals.new_chapters,
|
||||||
|
chapters.size,
|
||||||
|
chapters.size
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
chapters = chapters.joinToString("\n"),
|
||||||
|
manga = manga
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user