Migrate feed to adapter delegates

pull/26/head
Koitharu 6 years ago
parent 12c8cdfd70
commit b9f35f34ad

@ -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
}
} }
override fun getItemsCount(): Int { private fun onEmptyStateChanged(isEmpty: Boolean) {
return adapter?.itemCount ?: 0 if (isEmpty) {
setUpEmptyListHolder()
}
layout_holder.isVisible = isEmpty
} }
override fun onRequestMoreItems(offset: Int) { override fun onScrolledToEnd() {
viewModel.loadList(offset) viewModel.loadList(append = true)
} }
override fun onItemClick(item: TrackingLogItem, position: Int, view: View) { override fun onItemClick(item: Manga, view: View) {
startActivity(DetailsActivity.newIntent(context ?: return, item.manga)) startActivity(DetailsActivity.newIntent(context ?: return, item))
} }
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
)
Loading…
Cancel
Save