Migrate feed to adapter delegates

This commit is contained in:
Koitharu
2020-11-23 19:16:02 +02:00
parent 12c8cdfd70
commit b9f35f34ad
21 changed files with 237 additions and 158 deletions

View File

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

View File

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

View File

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

View File

@@ -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>) {

View File

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

View File

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

View File

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

View File

@@ -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? {

View File

@@ -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() {

View File

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

View File

@@ -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? {

View File

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

View File

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

View File

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

View File

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

View File

@@ -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() {

View File

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

View File

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

View File

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

View File

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

View File

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