Move list states to adapter delegates
This commit is contained in:
@@ -9,8 +9,11 @@ import org.koitharu.kotatsu.utils.ext.getThemeColor
|
||||
class ChipsFactory(val context: Context) {
|
||||
|
||||
fun create(
|
||||
convertView: Chip? = null, text: CharSequence, @DrawableRes iconRes: Int = 0,
|
||||
tag: Any? = null, onClickListener: View.OnClickListener? = null
|
||||
convertView: Chip? = null,
|
||||
text: CharSequence,
|
||||
@DrawableRes iconRes: Int = 0,
|
||||
tag: Any? = null,
|
||||
onClickListener: View.OnClickListener? = null
|
||||
): Chip {
|
||||
val chip = convertView ?: Chip(context).apply {
|
||||
setTextColor(context.getThemeColor(android.R.attr.textColorPrimary))
|
||||
|
||||
@@ -2,8 +2,9 @@ package org.koitharu.kotatsu.core.ui
|
||||
|
||||
import android.content.res.Resources
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
sealed class DateTimeAgo {
|
||||
sealed class DateTimeAgo : ListModel {
|
||||
|
||||
abstract fun format(resources: Resources): String
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import androidx.core.text.parseAsHtml
|
||||
import androidx.core.view.isVisible
|
||||
import coil.ImageLoader
|
||||
import coil.util.CoilUtils
|
||||
import com.google.android.material.chip.Chip
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -23,7 +22,6 @@ import org.koitharu.kotatsu.core.model.MangaHistory
|
||||
import org.koitharu.kotatsu.databinding.FragmentDetailsBinding
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesDialog
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||
import org.koitharu.kotatsu.search.ui.MangaSearchSheet
|
||||
import org.koitharu.kotatsu.utils.FileSizeUtils
|
||||
import org.koitharu.kotatsu.utils.ext.*
|
||||
import kotlin.math.roundToInt
|
||||
@@ -71,8 +69,7 @@ class DetailsFragment : BaseFragment<FragmentDetailsBinding>(), View.OnClickList
|
||||
create(
|
||||
text = it,
|
||||
iconRes = R.drawable.ic_chip_user,
|
||||
tag = it,
|
||||
onClickListener = this@DetailsFragment
|
||||
tag = it
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -80,8 +77,7 @@ class DetailsFragment : BaseFragment<FragmentDetailsBinding>(), View.OnClickList
|
||||
create(
|
||||
text = it.title,
|
||||
iconRes = R.drawable.ic_chip_tag,
|
||||
tag = it,
|
||||
onClickListener = this@DetailsFragment
|
||||
tag = it
|
||||
)
|
||||
}
|
||||
manga.url.toUri().toFileOrNull()?.let { f ->
|
||||
@@ -93,8 +89,7 @@ class DetailsFragment : BaseFragment<FragmentDetailsBinding>(), View.OnClickList
|
||||
create(
|
||||
text = FileSizeUtils.formatBytes(context, size),
|
||||
iconRes = R.drawable.ic_chip_storage,
|
||||
tag = it,
|
||||
onClickListener = this@DetailsFragment
|
||||
tag = it
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -134,11 +129,11 @@ class DetailsFragment : BaseFragment<FragmentDetailsBinding>(), View.OnClickList
|
||||
|
||||
override fun onClick(v: View) {
|
||||
val manga = viewModel.manga.value
|
||||
when {
|
||||
v.id == R.id.imageView_favourite -> {
|
||||
when (v.id) {
|
||||
R.id.imageView_favourite -> {
|
||||
FavouriteCategoriesDialog.show(childFragmentManager, manga ?: return)
|
||||
}
|
||||
v.id == R.id.button_read -> {
|
||||
R.id.button_read -> {
|
||||
startActivity(
|
||||
ReaderActivity.newIntent(
|
||||
context ?: return,
|
||||
@@ -147,15 +142,6 @@ class DetailsFragment : BaseFragment<FragmentDetailsBinding>(), View.OnClickList
|
||||
)
|
||||
)
|
||||
}
|
||||
v is Chip -> {
|
||||
when (val tag = v.tag) {
|
||||
is String -> MangaSearchSheet.show(
|
||||
activity?.supportFragmentManager
|
||||
?: childFragmentManager,
|
||||
manga?.source ?: return, tag
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,10 @@ class ChaptersAdapter(
|
||||
return items[position].chapter.id
|
||||
}
|
||||
|
||||
fun setItems(newItems: List<ChapterListItem>, callback: Runnable) {
|
||||
differ.submitList(newItems, callback)
|
||||
}
|
||||
|
||||
private class DiffCallback : DiffUtil.ItemCallback<ChapterListItem>() {
|
||||
|
||||
override fun areItemsTheSame(oldItem: ChapterListItem, newItem: ChapterListItem): Boolean {
|
||||
|
||||
@@ -23,17 +23,6 @@ class FavouritesListFragment : MangaListFragment() {
|
||||
|
||||
override fun onScrolledToEnd() = Unit
|
||||
|
||||
override fun setUpEmptyListHolder() {
|
||||
binding.textViewHolder.setText(
|
||||
if (categoryId == 0L) {
|
||||
R.string.you_have_not_favourites_yet
|
||||
} else {
|
||||
R.string.favourites_category_empty
|
||||
}
|
||||
)
|
||||
binding.textViewHolder.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
|
||||
}
|
||||
|
||||
override fun onCreatePopupMenu(inflater: MenuInflater, menu: Menu, data: Manga) {
|
||||
super.onCreatePopupMenu(inflater, menu, data)
|
||||
inflater.inflate(R.menu.popup_favourites, menu)
|
||||
|
||||
@@ -3,17 +3,18 @@ package org.koitharu.kotatsu.favourites.ui.list
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
||||
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
||||
import org.koitharu.kotatsu.list.ui.model.toGridModel
|
||||
import org.koitharu.kotatsu.list.ui.model.toListDetailedModel
|
||||
import org.koitharu.kotatsu.list.ui.model.toListModel
|
||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||
import org.koitharu.kotatsu.list.ui.model.toErrorState
|
||||
import org.koitharu.kotatsu.list.ui.model.toUi
|
||||
import org.koitharu.kotatsu.utils.ext.onFirst
|
||||
|
||||
class FavouritesListViewModel(
|
||||
@@ -26,19 +27,30 @@ class FavouritesListViewModel(
|
||||
if (categoryId == 0L) repository.observeAll() else repository.observeAll(categoryId),
|
||||
createListModeFlow()
|
||||
) { list, mode ->
|
||||
when (mode) {
|
||||
ListMode.LIST -> list.map { it.toListModel() }
|
||||
ListMode.DETAILED_LIST -> list.map { it.toListDetailedModel() }
|
||||
ListMode.GRID -> list.map { it.toGridModel() }
|
||||
when {
|
||||
list.isEmpty() -> listOf(
|
||||
EmptyState(
|
||||
if (categoryId == 0L) {
|
||||
R.string.you_have_not_favourites_yet
|
||||
} else {
|
||||
R.string.favourites_category_empty
|
||||
}
|
||||
)
|
||||
)
|
||||
else -> list.toUi(mode)
|
||||
}
|
||||
}.onEach {
|
||||
isEmptyState.postValue(it.isEmpty())
|
||||
}.onStart {
|
||||
isLoading.postValue(true)
|
||||
}.onFirst {
|
||||
isLoading.postValue(false)
|
||||
}.onStart {
|
||||
emit(listOf(LoadingState))
|
||||
}.catch {
|
||||
emit(listOf(it.toErrorState(canRetry = false)))
|
||||
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
|
||||
override fun onRefresh() = Unit
|
||||
|
||||
override fun onRetry() = Unit
|
||||
|
||||
fun removeFromFavourites(manga: Manga) {
|
||||
launchJob {
|
||||
if (categoryId == 0L) {
|
||||
|
||||
@@ -63,11 +63,6 @@ class HistoryListFragment : MangaListFragment() {
|
||||
return context?.getString(R.string.history)
|
||||
}
|
||||
|
||||
override fun setUpEmptyListHolder() {
|
||||
binding.textViewHolder.setText(R.string.text_history_holder)
|
||||
binding.textViewHolder.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
|
||||
}
|
||||
|
||||
override fun onCreatePopupMenu(inflater: MenuInflater, menu: Menu, data: Manga) {
|
||||
super.onCreatePopupMenu(inflater, menu, data)
|
||||
inflater.inflate(R.menu.popup_history, menu)
|
||||
|
||||
@@ -7,6 +7,7 @@ import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.*
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
@@ -14,9 +15,7 @@ import org.koitharu.kotatsu.core.ui.DateTimeAgo
|
||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||
import org.koitharu.kotatsu.history.domain.MangaWithHistory
|
||||
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
||||
import org.koitharu.kotatsu.list.ui.model.toGridModel
|
||||
import org.koitharu.kotatsu.list.ui.model.toListDetailedModel
|
||||
import org.koitharu.kotatsu.list.ui.model.toListModel
|
||||
import org.koitharu.kotatsu.list.ui.model.*
|
||||
import org.koitharu.kotatsu.utils.MangaShortcut
|
||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||
import org.koitharu.kotatsu.utils.ext.daysDiff
|
||||
@@ -44,16 +43,24 @@ class HistoryListViewModel(
|
||||
override val content = combine(
|
||||
repository.observeAllWithHistory(),
|
||||
historyGrouping,
|
||||
createListModeFlow(),
|
||||
::mapList
|
||||
).onEach {
|
||||
isEmptyState.postValue(it.isEmpty())
|
||||
}.onStart {
|
||||
isLoading.postValue(true)
|
||||
createListModeFlow()
|
||||
) { list, grouped, mode ->
|
||||
when {
|
||||
list.isEmpty() -> listOf(EmptyState(R.string.text_history_holder))
|
||||
else -> mapList(list, grouped, mode)
|
||||
}
|
||||
}.onFirst {
|
||||
isLoading.postValue(false)
|
||||
}.onStart {
|
||||
emit(listOf(LoadingState))
|
||||
}.catch {
|
||||
it.toErrorState(canRetry = false)
|
||||
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
|
||||
override fun onRefresh() = Unit
|
||||
|
||||
override fun onRetry() = Unit
|
||||
|
||||
fun clearHistory() {
|
||||
launchLoadingJob {
|
||||
repository.clear()
|
||||
@@ -77,8 +84,8 @@ class HistoryListViewModel(
|
||||
settings.historyGrouping = isGroupingEnabled
|
||||
}
|
||||
|
||||
private fun mapList(list: List<MangaWithHistory>, grouped: Boolean, mode: ListMode): List<Any> {
|
||||
val result = ArrayList<Any>((list.size * 1.4).toInt())
|
||||
private fun mapList(list: List<MangaWithHistory>, grouped: Boolean, mode: ListMode): List<ListModel> {
|
||||
val result = ArrayList<ListModel>(if (grouped) (list.size * 1.4).toInt() else list.size)
|
||||
var prevDate: DateTimeAgo? = null
|
||||
for ((manga, history) in list) {
|
||||
if (grouped) {
|
||||
|
||||
@@ -32,9 +32,9 @@ import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter
|
||||
import org.koitharu.kotatsu.list.ui.filter.FilterAdapter
|
||||
import org.koitharu.kotatsu.list.ui.filter.OnFilterChangedListener
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.utils.ext.clearItemDecorations
|
||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.utils.ext.hasItems
|
||||
import org.koitharu.kotatsu.utils.ext.toggleDrawer
|
||||
|
||||
abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
|
||||
@@ -62,7 +62,9 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding.drawer?.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
|
||||
listAdapter = MangaListAdapter(get(), viewLifecycleOwner, this)
|
||||
listAdapter = MangaListAdapter(get(), viewLifecycleOwner, this) {
|
||||
viewModel.onRetry()
|
||||
}
|
||||
paginationListener = PaginationScrollListener(4, this)
|
||||
with(binding.recyclerView) {
|
||||
setHasFixedSize(true)
|
||||
@@ -85,7 +87,6 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
|
||||
viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged)
|
||||
viewModel.listMode.observe(viewLifecycleOwner, ::onListModeChanged)
|
||||
viewModel.gridScale.observe(viewLifecycleOwner, ::onGridScaleChanged)
|
||||
viewModel.isEmptyState.observe(viewLifecycleOwner, ::onEmptyStateChanged)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
@@ -140,9 +141,10 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
|
||||
@CallSuper
|
||||
override fun onRefresh() {
|
||||
binding.swipeRefreshLayout.isRefreshing = true
|
||||
viewModel.onRefresh()
|
||||
}
|
||||
|
||||
private fun onListChanged(list: List<Any>) {
|
||||
private fun onListChanged(list: List<ListModel>) {
|
||||
spanSizeLookup.invalidateCache()
|
||||
listAdapter?.items = list
|
||||
}
|
||||
@@ -150,45 +152,24 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
|
||||
private fun onError(e: Throwable) {
|
||||
if (e is CloudFlareProtectedException) {
|
||||
CloudFlareDialog.newInstance(e.url).show(childFragmentManager, CloudFlareDialog.TAG)
|
||||
}
|
||||
if (viewModel.isEmptyState.value == true) {
|
||||
binding.textViewHolder.text = e.getDisplayMessage(resources)
|
||||
binding.textViewHolder.setCompoundDrawablesRelativeWithIntrinsicBounds(
|
||||
0,
|
||||
R.drawable.ic_error_large,
|
||||
0,
|
||||
0
|
||||
)
|
||||
binding.textViewHolder.isVisible = true
|
||||
} else {
|
||||
Snackbar.make(
|
||||
binding.recyclerView,
|
||||
e.getDisplayMessage(resources),
|
||||
Snackbar.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
protected open fun onLoadingStateChanged(isLoading: Boolean) {
|
||||
val hasItems = binding.recyclerView.hasItems
|
||||
binding.progressBar.isVisible =
|
||||
isLoading && !hasItems && viewModel.isEmptyState.value != true
|
||||
binding.swipeRefreshLayout.isEnabled =
|
||||
isSwipeRefreshEnabled && !binding.progressBar.isVisible
|
||||
isSwipeRefreshEnabled && !isLoading
|
||||
if (!isLoading) {
|
||||
binding.swipeRefreshLayout.isRefreshing = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun onEmptyStateChanged(isEmpty: Boolean) {
|
||||
if (isEmpty) {
|
||||
setUpEmptyListHolder()
|
||||
}
|
||||
binding.layoutHolder.isVisible = isEmpty
|
||||
}
|
||||
|
||||
protected fun onInitFilter(config: MangaFilterConfig) {
|
||||
binding.recyclerViewFilter.adapter = FilterAdapter(
|
||||
sortOrders = config.sortOrders,
|
||||
@@ -214,13 +195,6 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
|
||||
binding.drawer?.closeDrawers()
|
||||
}
|
||||
|
||||
protected open fun setUpEmptyListHolder() {
|
||||
with(binding.textViewHolder) {
|
||||
setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, null, null)
|
||||
setText(R.string.nothing_found)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onGridScaleChanged(scale: Float) {
|
||||
spanSizeLookup.invalidateCache()
|
||||
spanResolver.setGridSize(scale, binding.recyclerView)
|
||||
@@ -293,9 +267,8 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
|
||||
val total =
|
||||
(binding.recyclerView.layoutManager as? GridLayoutManager)?.spanCount ?: return 1
|
||||
return when (listAdapter?.getItemViewType(position)) {
|
||||
MangaListAdapter.ITEM_TYPE_DATE,
|
||||
MangaListAdapter.ITEM_TYPE_PROGRESS -> total
|
||||
else -> 1
|
||||
MangaListAdapter.ITEM_TYPE_MANGA_GRID -> 1
|
||||
else -> total
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.koitharu.kotatsu.list.ui
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
@@ -16,28 +15,30 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import org.koin.android.ext.android.get
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseBottomSheet
|
||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.base.ui.list.PaginationScrollListener
|
||||
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
|
||||
import org.koitharu.kotatsu.browser.cloudflare.CloudFlareDialog
|
||||
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
import org.koitharu.kotatsu.databinding.SheetListBinding
|
||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter
|
||||
import org.koitharu.kotatsu.utils.UiUtils
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.utils.ext.*
|
||||
|
||||
abstract class MangaListSheet : BaseBottomSheet<SheetListBinding>(),
|
||||
PaginationScrollListener.Callback, OnListItemClickListener<Manga>,
|
||||
SharedPreferences.OnSharedPreferenceChangeListener, Toolbar.OnMenuItemClickListener {
|
||||
Toolbar.OnMenuItemClickListener {
|
||||
|
||||
private val settings by inject<AppSettings>()
|
||||
|
||||
private var adapter: MangaListAdapter? = null
|
||||
private var listAdapter: MangaListAdapter? = null
|
||||
private var paginationListener: PaginationScrollListener? = null
|
||||
private val spanResolver = MangaListSpanResolver()
|
||||
private val spanSizeLookup = SpanSizeLookup()
|
||||
open val isSwipeRefreshEnabled = true
|
||||
|
||||
protected abstract val viewModel: MangaListViewModel
|
||||
|
||||
@@ -47,33 +48,39 @@ abstract class MangaListSheet : BaseBottomSheet<SheetListBinding>(),
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
adapter = MangaListAdapter(get(), viewLifecycleOwner, this)
|
||||
initListMode(settings.listMode)
|
||||
binding.recyclerView.adapter = adapter
|
||||
binding.recyclerView.addOnScrollListener(PaginationScrollListener(4, this))
|
||||
settings.subscribe(this)
|
||||
binding.toolbar.inflateMenu(R.menu.opt_list_sheet)
|
||||
binding.toolbar.setOnMenuItemClickListener(this)
|
||||
binding.toolbar.setNavigationOnClickListener {
|
||||
dismiss()
|
||||
listAdapter = MangaListAdapter(get(), viewLifecycleOwner, this) {
|
||||
viewModel.onRetry()
|
||||
}
|
||||
paginationListener = PaginationScrollListener(4, this)
|
||||
with(binding.recyclerView) {
|
||||
setHasFixedSize(true)
|
||||
adapter = listAdapter
|
||||
addOnScrollListener(paginationListener!!)
|
||||
}
|
||||
with(binding.toolbar) {
|
||||
inflateMenu(R.menu.opt_list_sheet)
|
||||
setOnMenuItemClickListener(this@MangaListSheet)
|
||||
setNavigationOnClickListener {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
if (dialog !is BottomSheetDialog) {
|
||||
binding.toolbar.isVisible = true
|
||||
binding.textViewTitle.isVisible = false
|
||||
binding.appbar.elevation = resources.getDimension(R.dimen.elevation_large)
|
||||
}
|
||||
if (savedInstanceState == null) {
|
||||
onScrolledToEnd()
|
||||
}
|
||||
|
||||
viewModel.content.observe(viewLifecycleOwner, ::onListChanged)
|
||||
viewModel.onError.observe(viewLifecycleOwner, ::onError)
|
||||
viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged)
|
||||
viewModel.listMode.observe(viewLifecycleOwner, ::initListMode)
|
||||
viewModel.listMode.observe(viewLifecycleOwner, ::onListModeChanged)
|
||||
viewModel.gridScale.observe(viewLifecycleOwner, ::onGridScaleChanged)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
settings.unsubscribe(this)
|
||||
adapter = null
|
||||
listAdapter = null
|
||||
paginationListener = null
|
||||
spanSizeLookup.invalidateCache()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
@@ -117,65 +124,94 @@ abstract class MangaListSheet : BaseBottomSheet<SheetListBinding>(),
|
||||
else -> false
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
|
||||
when (key) {
|
||||
AppSettings.KEY_LIST_MODE -> initListMode(settings.listMode)
|
||||
AppSettings.KEY_GRID_SIZE -> UiUtils.SpanCountResolver.update(binding.recyclerView)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemClick(item: Manga, view: View) {
|
||||
startActivity(DetailsActivity.newIntent(context ?: return, item))
|
||||
}
|
||||
|
||||
private fun onListChanged(list: List<Any>) {
|
||||
adapter?.items = list
|
||||
binding.textViewHolder.isVisible = list.isEmpty()
|
||||
binding.recyclerView.callOnScrollListeners()
|
||||
private fun onListChanged(list: List<ListModel>) {
|
||||
spanSizeLookup.invalidateCache()
|
||||
listAdapter?.items = list
|
||||
}
|
||||
|
||||
private fun onError(e: Throwable) {
|
||||
Snackbar.make(binding.recyclerView, e.getDisplayMessage(resources), Snackbar.LENGTH_SHORT)
|
||||
.show()
|
||||
if (e is CloudFlareProtectedException) {
|
||||
CloudFlareDialog.newInstance(e.url).show(childFragmentManager, CloudFlareDialog.TAG)
|
||||
} else {
|
||||
Snackbar.make(
|
||||
binding.recyclerView,
|
||||
e.getDisplayMessage(resources),
|
||||
Snackbar.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onLoadingStateChanged(isLoading: Boolean) {
|
||||
binding.progressBar.isVisible = isLoading && !binding.recyclerView.hasItems
|
||||
if (isLoading) {
|
||||
binding.textViewHolder.isVisible = false
|
||||
binding.progressBar.isVisible =
|
||||
isLoading && !binding.recyclerView.hasItems
|
||||
}
|
||||
|
||||
private fun onGridScaleChanged(scale: Float) {
|
||||
spanSizeLookup.invalidateCache()
|
||||
spanResolver.setGridSize(scale, binding.recyclerView)
|
||||
}
|
||||
|
||||
private fun onListModeChanged(mode: ListMode) {
|
||||
spanSizeLookup.invalidateCache()
|
||||
with(binding.recyclerView) {
|
||||
clearItemDecorations()
|
||||
removeOnLayoutChangeListener(spanResolver)
|
||||
when (mode) {
|
||||
ListMode.LIST -> {
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
addItemDecoration(
|
||||
DividerItemDecoration(
|
||||
context,
|
||||
RecyclerView.VERTICAL
|
||||
)
|
||||
)
|
||||
}
|
||||
ListMode.DETAILED_LIST -> {
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
addItemDecoration(
|
||||
SpacingItemDecoration(
|
||||
resources.getDimensionPixelOffset(R.dimen.grid_spacing)
|
||||
)
|
||||
)
|
||||
}
|
||||
ListMode.GRID -> {
|
||||
layoutManager = GridLayoutManager(context, spanResolver.spanCount).also {
|
||||
it.spanSizeLookup = spanSizeLookup
|
||||
}
|
||||
addItemDecoration(
|
||||
SpacingItemDecoration(
|
||||
resources.getDimensionPixelOffset(R.dimen.grid_spacing)
|
||||
)
|
||||
)
|
||||
addOnLayoutChangeListener(spanResolver)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initListMode(mode: ListMode) {
|
||||
val ctx = context ?: return
|
||||
val position = binding.recyclerView.firstItem
|
||||
binding.recyclerView.layoutManager = null
|
||||
binding.recyclerView.clearItemDecorations()
|
||||
binding.recyclerView.removeOnLayoutChangeListener(UiUtils.SpanCountResolver)
|
||||
binding.recyclerView.layoutManager = when (mode) {
|
||||
ListMode.GRID -> {
|
||||
GridLayoutManager(ctx, UiUtils.resolveGridSpanCount(ctx)).apply {
|
||||
spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
||||
override fun getSpanSize(position: Int) = if (position < TODO() as Int)
|
||||
1 else this@apply.spanCount
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> LinearLayoutManager(ctx)
|
||||
private inner class SpanSizeLookup : GridLayoutManager.SpanSizeLookup() {
|
||||
|
||||
init {
|
||||
isSpanIndexCacheEnabled = true
|
||||
isSpanGroupIndexCacheEnabled = true
|
||||
}
|
||||
binding.recyclerView.addItemDecoration(
|
||||
when (mode) {
|
||||
ListMode.LIST -> DividerItemDecoration(ctx, RecyclerView.VERTICAL)
|
||||
ListMode.DETAILED_LIST,
|
||||
ListMode.GRID -> SpacingItemDecoration(
|
||||
resources.getDimensionPixelOffset(R.dimen.grid_spacing)
|
||||
)
|
||||
|
||||
override fun getSpanSize(position: Int): Int {
|
||||
val total =
|
||||
(binding.recyclerView.layoutManager as? GridLayoutManager)?.spanCount ?: return 1
|
||||
return when (listAdapter?.getItemViewType(position)) {
|
||||
MangaListAdapter.ITEM_TYPE_MANGA_GRID -> 1
|
||||
else -> total
|
||||
}
|
||||
)
|
||||
if (mode == ListMode.GRID) {
|
||||
binding.recyclerView.addOnLayoutChangeListener(UiUtils.SpanCountResolver)
|
||||
}
|
||||
adapter?.notifyDataSetChanged()
|
||||
binding.recyclerView.firstItem = position
|
||||
|
||||
fun invalidateCache() {
|
||||
invalidateSpanGroupIndexCache()
|
||||
invalidateSpanIndexCache()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,13 +9,13 @@ import kotlinx.coroutines.flow.*
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
abstract class MangaListViewModel(
|
||||
private val settings: AppSettings
|
||||
) : BaseViewModel() {
|
||||
|
||||
abstract val content: LiveData<List<Any>>
|
||||
val isEmptyState = MutableLiveData(false)
|
||||
abstract val content: LiveData<List<ListModel>>
|
||||
val filter = MutableLiveData<MangaFilterConfig>()
|
||||
val listMode = MutableLiveData<ListMode>()
|
||||
val gridScale = settings.observe()
|
||||
@@ -30,4 +30,8 @@ abstract class MangaListViewModel(
|
||||
.onStart { emit(settings.listMode) }
|
||||
.distinctUntilChanged()
|
||||
.onEach { listMode.postValue(it) }
|
||||
|
||||
abstract fun onRefresh()
|
||||
|
||||
abstract fun onRetry()
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.koitharu.kotatsu.list.ui.adapter
|
||||
|
||||
import android.widget.TextView
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
fun emptyStateListAD() = adapterDelegate<EmptyState, ListModel>(R.layout.item_empty_state) {
|
||||
|
||||
bind {
|
||||
(itemView as TextView).setText(item.text)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.koitharu.kotatsu.list.ui.adapter
|
||||
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.databinding.ItemErrorFooterBinding
|
||||
import org.koitharu.kotatsu.list.ui.model.ErrorFooter
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
|
||||
fun errorFooterAD(
|
||||
onRetryClick: () -> Unit
|
||||
) = adapterDelegateViewBinding<ErrorFooter, ListModel, ItemErrorFooterBinding>(
|
||||
{ inflater, parent -> ItemErrorFooterBinding.inflate(inflater, parent, false) }
|
||||
) {
|
||||
|
||||
binding.root.setOnClickListener {
|
||||
onRetryClick()
|
||||
}
|
||||
|
||||
bind {
|
||||
binding.textViewTitle.text = item.exception.getDisplayMessage(context.resources)
|
||||
binding.imageViewIcon.setImageResource(item.icon)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.koitharu.kotatsu.list.ui.adapter
|
||||
|
||||
import androidx.core.view.isVisible
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.databinding.ItemErrorStateBinding
|
||||
import org.koitharu.kotatsu.list.ui.model.ErrorState
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
|
||||
fun errorStateListAD(
|
||||
onRetryClick: () -> Unit
|
||||
) = adapterDelegateViewBinding<ErrorState, ListModel, ItemErrorStateBinding>(
|
||||
{ inflater, parent -> ItemErrorStateBinding.inflate(inflater, parent, false) }
|
||||
) {
|
||||
|
||||
binding.buttonRetry.setOnClickListener {
|
||||
onRetryClick()
|
||||
}
|
||||
|
||||
bind {
|
||||
with(binding.textViewError) {
|
||||
text = item.exception.getDisplayMessage(context.resources)
|
||||
setCompoundDrawablesWithIntrinsicBounds(0, item.icon, 0, 0)
|
||||
}
|
||||
binding.buttonRetry.isVisible = item.canRetry
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package org.koitharu.kotatsu.list.ui.adapter
|
||||
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.list.ui.model.IndeterminateProgress
|
||||
|
||||
fun indeterminateProgressAD() = adapterDelegate<IndeterminateProgress, Any>(R.layout.item_progress) {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.koitharu.kotatsu.list.ui.adapter
|
||||
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
|
||||
|
||||
fun loadingFooterAD() = adapterDelegate<LoadingFooter, ListModel>(R.layout.item_loading_footer) {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.koitharu.kotatsu.list.ui.adapter
|
||||
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||
|
||||
fun loadingStateAD() = adapterDelegate<LoadingState, ListModel>(R.layout.item_loading_state) {
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.databinding.ItemMangaGridBinding
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.model.MangaGridModel
|
||||
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.utils.ext.newImageRequest
|
||||
@@ -17,7 +18,7 @@ fun mangaGridItemAD(
|
||||
coil: ImageLoader,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
clickListener: OnListItemClickListener<Manga>
|
||||
) = adapterDelegateViewBinding<MangaGridModel, Any, ItemMangaGridBinding>(
|
||||
) = adapterDelegateViewBinding<MangaGridModel, ListModel, ItemMangaGridBinding>(
|
||||
{ inflater, parent -> ItemMangaGridBinding.inflate(inflater, parent, false) }
|
||||
) {
|
||||
|
||||
|
||||
@@ -6,8 +6,7 @@ import coil.ImageLoader
|
||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.ui.DateTimeAgo
|
||||
import org.koitharu.kotatsu.list.ui.model.IndeterminateProgress
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.model.MangaGridModel
|
||||
import org.koitharu.kotatsu.list.ui.model.MangaListDetailedModel
|
||||
import org.koitharu.kotatsu.list.ui.model.MangaListModel
|
||||
@@ -16,8 +15,9 @@ import kotlin.jvm.internal.Intrinsics
|
||||
class MangaListAdapter(
|
||||
coil: ImageLoader,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
clickListener: OnListItemClickListener<Manga>
|
||||
) : AsyncListDifferDelegationAdapter<Any>(DiffCallback()) {
|
||||
clickListener: OnListItemClickListener<Manga>,
|
||||
onRetryClick: () -> Unit
|
||||
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()) {
|
||||
|
||||
init {
|
||||
delegatesManager
|
||||
@@ -30,13 +30,17 @@ class MangaListAdapter(
|
||||
mangaListDetailedItemAD(coil, lifecycleOwner, clickListener)
|
||||
)
|
||||
.addDelegate(ITEM_TYPE_MANGA_GRID, mangaGridItemAD(coil, lifecycleOwner, clickListener))
|
||||
.addDelegate(ITEM_TYPE_PROGRESS, indeterminateProgressAD())
|
||||
.addDelegate(ITEM_TYPE_LOADING_FOOTER, loadingFooterAD())
|
||||
.addDelegate(ITEM_TYPE_LOADING_STATE, loadingStateAD())
|
||||
.addDelegate(ITEM_TYPE_DATE, relatedDateItemAD())
|
||||
.addDelegate(ITEM_TYPE_ERROR_STATE, errorStateListAD(onRetryClick))
|
||||
.addDelegate(ITEM_TYPE_ERROR_FOOTER, errorFooterAD(onRetryClick))
|
||||
.addDelegate(ITEM_TYPE_EMPTY, emptyStateListAD())
|
||||
}
|
||||
|
||||
private class DiffCallback : DiffUtil.ItemCallback<Any>() {
|
||||
private class DiffCallback : DiffUtil.ItemCallback<ListModel>() {
|
||||
|
||||
override fun areItemsTheSame(oldItem: Any, newItem: Any) = when {
|
||||
override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel) = when {
|
||||
oldItem is MangaListModel && newItem is MangaListModel -> {
|
||||
oldItem.id == newItem.id
|
||||
}
|
||||
@@ -46,16 +50,10 @@ class MangaListAdapter(
|
||||
oldItem is MangaGridModel && newItem is MangaGridModel -> {
|
||||
oldItem.id == newItem.id
|
||||
}
|
||||
oldItem == IndeterminateProgress && newItem == IndeterminateProgress -> {
|
||||
true
|
||||
}
|
||||
oldItem is DateTimeAgo && newItem is DateTimeAgo -> {
|
||||
oldItem == newItem
|
||||
}
|
||||
else -> false
|
||||
else -> oldItem.javaClass == newItem.javaClass
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: Any, newItem: Any): Boolean {
|
||||
override fun areContentsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
|
||||
return Intrinsics.areEqual(oldItem, newItem)
|
||||
}
|
||||
}
|
||||
@@ -65,7 +63,11 @@ class MangaListAdapter(
|
||||
const val ITEM_TYPE_MANGA_LIST = 0
|
||||
const val ITEM_TYPE_MANGA_LIST_DETAILED = 1
|
||||
const val ITEM_TYPE_MANGA_GRID = 2
|
||||
const val ITEM_TYPE_PROGRESS = 3
|
||||
const val ITEM_TYPE_DATE = 4
|
||||
const val ITEM_TYPE_LOADING_FOOTER = 3
|
||||
const val ITEM_TYPE_LOADING_STATE = 4
|
||||
const val ITEM_TYPE_DATE = 5
|
||||
const val ITEM_TYPE_ERROR_STATE = 6
|
||||
const val ITEM_TYPE_ERROR_FOOTER = 7
|
||||
const val ITEM_TYPE_EMPTY = 8
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.databinding.ItemMangaListDetailsBinding
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.model.MangaListDetailedModel
|
||||
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.utils.ext.newImageRequest
|
||||
@@ -18,7 +19,7 @@ fun mangaListDetailedItemAD(
|
||||
coil: ImageLoader,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
clickListener: OnListItemClickListener<Manga>
|
||||
) = adapterDelegateViewBinding<MangaListDetailedModel, Any, ItemMangaListDetailsBinding>(
|
||||
) = adapterDelegateViewBinding<MangaListDetailedModel, ListModel, ItemMangaListDetailsBinding>(
|
||||
{ inflater, parent -> ItemMangaListDetailsBinding.inflate(inflater, parent, false) }
|
||||
) {
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.databinding.ItemMangaListBinding
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.model.MangaListModel
|
||||
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.utils.ext.newImageRequest
|
||||
@@ -18,7 +19,7 @@ fun mangaListItemAD(
|
||||
coil: ImageLoader,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
clickListener: OnListItemClickListener<Manga>
|
||||
) = adapterDelegateViewBinding<MangaListModel, Any, ItemMangaListBinding>(
|
||||
) = adapterDelegateViewBinding<MangaListModel, ListModel, ItemMangaListBinding>(
|
||||
{ inflater, parent -> ItemMangaListBinding.inflate(inflater, parent, false) }
|
||||
) {
|
||||
|
||||
|
||||
@@ -4,8 +4,9 @@ import android.widget.TextView
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.DateTimeAgo
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
fun relatedDateItemAD() = adapterDelegate<DateTimeAgo, Any>(R.layout.item_header) {
|
||||
fun relatedDateItemAD() = adapterDelegate<DateTimeAgo, ListModel>(R.layout.item_header) {
|
||||
|
||||
bind {
|
||||
(itemView as TextView).text = item.format(context.resources)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.koitharu.kotatsu.list.ui.model
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
|
||||
data class EmptyState(
|
||||
@StringRes val text: Int
|
||||
) : ListModel
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.koitharu.kotatsu.list.ui.model
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
|
||||
data class ErrorFooter(
|
||||
val exception: Throwable,
|
||||
@DrawableRes val icon: Int
|
||||
) : ListModel
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.koitharu.kotatsu.list.ui.model
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
|
||||
data class ErrorState(
|
||||
val exception: Throwable,
|
||||
@DrawableRes val icon: Int,
|
||||
val canRetry: Boolean
|
||||
) : ListModel
|
||||
@@ -1,3 +1,3 @@
|
||||
package org.koitharu.kotatsu.list.ui.model
|
||||
|
||||
object IndeterminateProgress
|
||||
interface ListModel
|
||||
@@ -1,6 +1,8 @@
|
||||
package org.koitharu.kotatsu.list.ui.model
|
||||
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
fun Manga.toListModel() = MangaListModel(
|
||||
@@ -26,4 +28,27 @@ fun Manga.toGridModel() = MangaGridModel(
|
||||
title = title,
|
||||
coverUrl = coverUrl,
|
||||
manga = this
|
||||
)
|
||||
|
||||
fun List<Manga>.toUi(mode: ListMode): List<ListModel> = when(mode) {
|
||||
ListMode.LIST -> map(Manga::toListModel)
|
||||
ListMode.DETAILED_LIST -> map(Manga::toListDetailedModel)
|
||||
ListMode.GRID -> map(Manga::toGridModel)
|
||||
}
|
||||
|
||||
fun <C : MutableCollection<ListModel>> List<Manga>.toUi(destination: C, mode: ListMode): C = when(mode) {
|
||||
ListMode.LIST -> mapTo(destination, Manga::toListModel)
|
||||
ListMode.DETAILED_LIST -> mapTo(destination, Manga::toListDetailedModel)
|
||||
ListMode.GRID -> mapTo(destination, Manga::toGridModel)
|
||||
}
|
||||
|
||||
fun Throwable.toErrorState(canRetry: Boolean = true) = ErrorState(
|
||||
exception = this,
|
||||
icon = R.drawable.ic_error_large,
|
||||
canRetry = canRetry
|
||||
)
|
||||
|
||||
fun Throwable.toErrorFooter() = ErrorFooter(
|
||||
exception = this,
|
||||
icon = R.drawable.ic_alert_outline
|
||||
)
|
||||
@@ -0,0 +1,3 @@
|
||||
package org.koitharu.kotatsu.list.ui.model
|
||||
|
||||
object LoadingFooter : ListModel
|
||||
@@ -0,0 +1,3 @@
|
||||
package org.koitharu.kotatsu.list.ui.model
|
||||
|
||||
object LoadingState : ListModel
|
||||
@@ -7,4 +7,4 @@ data class MangaGridModel(
|
||||
val title: String,
|
||||
val coverUrl: String,
|
||||
val manga: Manga
|
||||
)
|
||||
) : ListModel
|
||||
@@ -10,4 +10,4 @@ data class MangaListDetailedModel(
|
||||
val coverUrl: String,
|
||||
val rating: String?,
|
||||
val manga: Manga
|
||||
)
|
||||
) : ListModel
|
||||
@@ -8,4 +8,4 @@ data class MangaListModel(
|
||||
val subtitle: String,
|
||||
val coverUrl: String,
|
||||
val manga: Manga
|
||||
)
|
||||
) : ListModel
|
||||
@@ -31,11 +31,6 @@ class LocalListFragment : MangaListFragment(), ActivityResultCallback<Uri> {
|
||||
viewModel.onMangaRemoved.observe(viewLifecycleOwner, ::onItemRemoved)
|
||||
}
|
||||
|
||||
override fun onRefresh() {
|
||||
super.onRefresh()
|
||||
viewModel.onRefresh()
|
||||
}
|
||||
|
||||
override fun onScrolledToEnd() = Unit
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
@@ -68,11 +63,6 @@ class LocalListFragment : MangaListFragment(), ActivityResultCallback<Uri> {
|
||||
return context?.getString(R.string.local_storage)
|
||||
}
|
||||
|
||||
override fun setUpEmptyListHolder() {
|
||||
binding.textViewHolder.setText(R.string.text_local_holder)
|
||||
binding.textViewHolder.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
|
||||
}
|
||||
|
||||
override fun onActivityResult(result: Uri?) {
|
||||
if (result != null) {
|
||||
viewModel.importFile(result)
|
||||
|
||||
@@ -8,16 +8,18 @@ import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
||||
import org.koitharu.kotatsu.list.ui.model.toGridModel
|
||||
import org.koitharu.kotatsu.list.ui.model.toListDetailedModel
|
||||
import org.koitharu.kotatsu.list.ui.model.toListModel
|
||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||
import org.koitharu.kotatsu.list.ui.model.toErrorState
|
||||
import org.koitharu.kotatsu.list.ui.model.toUi
|
||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.utils.MangaShortcut
|
||||
import org.koitharu.kotatsu.utils.MediaStoreCompat
|
||||
@@ -34,30 +36,41 @@ class LocalListViewModel(
|
||||
) : MangaListViewModel(settings) {
|
||||
|
||||
val onMangaRemoved = SingleLiveEvent<Manga>()
|
||||
private val mangaList = MutableStateFlow<List<Manga>>(emptyList())
|
||||
private val listError = MutableStateFlow<Throwable?>(null)
|
||||
private val mangaList = MutableStateFlow<List<Manga>?>(null)
|
||||
|
||||
override val content = combine(mangaList, createListModeFlow()) { list, mode ->
|
||||
when (mode) {
|
||||
ListMode.LIST -> list.map { it.toListModel() }
|
||||
ListMode.DETAILED_LIST -> list.map { it.toListDetailedModel() }
|
||||
ListMode.GRID -> list.map { it.toGridModel() }
|
||||
override val content = combine(
|
||||
mangaList,
|
||||
createListModeFlow(),
|
||||
listError
|
||||
) { list, mode, error ->
|
||||
when {
|
||||
error != null -> listOf(error.toErrorState(canRetry = true))
|
||||
list == null -> listOf(LoadingState)
|
||||
list.isEmpty() -> listOf(EmptyState(R.string.text_local_holder))
|
||||
else -> list.toUi(mode)
|
||||
}
|
||||
}.onStart {
|
||||
emit(listOf(LoadingState))
|
||||
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
|
||||
init {
|
||||
onRefresh()
|
||||
}
|
||||
|
||||
fun onRefresh() {
|
||||
launchLoadingJob {
|
||||
withContext(Dispatchers.Default) {
|
||||
val list = repository.getList(0)
|
||||
mangaList.value = list
|
||||
isEmptyState.postValue(list.isEmpty())
|
||||
override fun onRefresh() {
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
try {
|
||||
listError.value = null
|
||||
mangaList.value = repository.getList(0)
|
||||
} catch (e: Throwable) {
|
||||
listError.value = e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRetry() = onRefresh()
|
||||
|
||||
fun importFile(uri: Uri) {
|
||||
launchLoadingJob {
|
||||
val contentResolver = context.contentResolver
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
|
||||
@@ -14,6 +15,8 @@ import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.model.MangaChapter
|
||||
import org.koitharu.kotatsu.databinding.DialogChaptersBinding
|
||||
import org.koitharu.kotatsu.details.ui.adapter.ChaptersAdapter
|
||||
import org.koitharu.kotatsu.details.ui.model.toListItem
|
||||
import org.koitharu.kotatsu.history.domain.ChapterExtra
|
||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||
|
||||
class ChaptersDialog : AlertDialogFragment<DialogChaptersBinding>(),
|
||||
@@ -32,14 +35,34 @@ class ChaptersDialog : AlertDialogFragment<DialogChaptersBinding>(),
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
binding.recyclerViewChapters.addItemDecoration(
|
||||
DividerItemDecoration(
|
||||
requireContext(),
|
||||
RecyclerView.VERTICAL
|
||||
)
|
||||
DividerItemDecoration(requireContext(), RecyclerView.VERTICAL)
|
||||
)
|
||||
val chapters = arguments?.getParcelableArrayList<MangaChapter>(ARG_CHAPTERS)
|
||||
if (chapters == null) {
|
||||
dismissAllowingStateLoss()
|
||||
return
|
||||
}
|
||||
val currentId = arguments?.getLong(ARG_CURRENT_ID, 0L) ?: 0L
|
||||
val currentPosition = chapters.indexOfFirst { it.id == currentId }
|
||||
binding.recyclerViewChapters.adapter = ChaptersAdapter(this).apply {
|
||||
// arguments?.getParcelableArrayList<MangaChapter>(ARG_CHAPTERS)?.let(this::setItems)
|
||||
// currentChapterId = arguments?.getLong(ARG_CURRENT_ID, 0L)?.takeUnless { it == 0L }
|
||||
setItems(chapters.mapIndexed { index, chapter ->
|
||||
chapter.toListItem(
|
||||
when {
|
||||
index < currentPosition -> ChapterExtra.READ
|
||||
index == currentPosition -> ChapterExtra.CURRENT
|
||||
else -> ChapterExtra.UNREAD
|
||||
}
|
||||
)
|
||||
}) {
|
||||
if (currentPosition >= 0) {
|
||||
with(binding.recyclerViewChapters) {
|
||||
(layoutManager as LinearLayoutManager).scrollToPositionWithOffset(
|
||||
currentPosition,
|
||||
height / 3
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,10 +87,9 @@ class ChaptersDialog : AlertDialogFragment<DialogChaptersBinding>(),
|
||||
private const val ARG_CURRENT_ID = "current_id"
|
||||
|
||||
fun show(fm: FragmentManager, chapters: List<MangaChapter>, currentId: Long = 0L) =
|
||||
ChaptersDialog()
|
||||
.withArgs(2) {
|
||||
putParcelableArrayList(ARG_CHAPTERS, ArrayList(chapters))
|
||||
putLong(ARG_CURRENT_ID, currentId)
|
||||
}.show(fm, TAG)
|
||||
ChaptersDialog().withArgs(2) {
|
||||
putParcelableArrayList(ARG_CHAPTERS, ArrayList(chapters))
|
||||
putLong(ARG_CURRENT_ID, currentId)
|
||||
}.show(fm, TAG)
|
||||
}
|
||||
}
|
||||
@@ -21,13 +21,8 @@ class RemoteListFragment : MangaListFragment() {
|
||||
|
||||
private val source by parcelableArgument<MangaSource>(ARG_SOURCE)
|
||||
|
||||
override fun onRefresh() {
|
||||
super.onRefresh()
|
||||
viewModel.loadList(append = false)
|
||||
}
|
||||
|
||||
override fun onScrolledToEnd() {
|
||||
viewModel.loadList(append = true)
|
||||
viewModel.loadNextPage()
|
||||
}
|
||||
|
||||
override fun getTitle(): CharSequence? {
|
||||
|
||||
@@ -6,41 +6,51 @@ 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 kotlinx.coroutines.flow.onStart
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.model.MangaFilter
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
import org.koitharu.kotatsu.list.ui.MangaFilterConfig
|
||||
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
||||
import org.koitharu.kotatsu.list.ui.model.IndeterminateProgress
|
||||
import org.koitharu.kotatsu.list.ui.model.toGridModel
|
||||
import org.koitharu.kotatsu.list.ui.model.toListDetailedModel
|
||||
import org.koitharu.kotatsu.list.ui.model.toListModel
|
||||
import org.koitharu.kotatsu.list.ui.model.*
|
||||
import java.util.*
|
||||
|
||||
class RemoteListViewModel(
|
||||
private val repository: MangaRepository,
|
||||
settings: AppSettings
|
||||
) : MangaListViewModel(settings) {
|
||||
|
||||
private val mangaList = MutableStateFlow<List<Manga>>(emptyList())
|
||||
private val mangaList = MutableStateFlow<List<Manga>?>(null)
|
||||
private val hasNextPage = MutableStateFlow(false)
|
||||
private val listError = MutableStateFlow<Throwable?>(null)
|
||||
private var appliedFilter: MangaFilter? = null
|
||||
private var loadingJob: Job? = null
|
||||
|
||||
override val content = combine(mangaList.drop(1), createListModeFlow()) { list, mode ->
|
||||
when (mode) {
|
||||
ListMode.LIST -> list.map { it.toListModel() }
|
||||
ListMode.DETAILED_LIST -> list.map { it.toListDetailedModel() }
|
||||
ListMode.GRID -> list.map { it.toGridModel() }
|
||||
override val content = combine(
|
||||
mangaList,
|
||||
createListModeFlow(),
|
||||
listError,
|
||||
hasNextPage
|
||||
) { list, mode, error, hasNext ->
|
||||
when {
|
||||
list.isNullOrEmpty() && error != null -> listOf(error.toErrorState(canRetry = true))
|
||||
list == null -> listOf(LoadingState)
|
||||
list.isEmpty() -> listOf(EmptyState(R.string.nothing_found))
|
||||
else -> {
|
||||
val result = ArrayList<ListModel>(list.size + 1)
|
||||
list.toUi(result, mode)
|
||||
when {
|
||||
error != null -> result += error.toErrorFooter()
|
||||
hasNext -> result += LoadingFooter
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
}.onEach {
|
||||
isEmptyState.postValue(it.isEmpty())
|
||||
}.combine(hasNextPage) { list, isHasNextPage ->
|
||||
if (isHasNextPage && list.isNotEmpty()) list + IndeterminateProgress else list
|
||||
}.onStart {
|
||||
emit(listOf(LoadingState))
|
||||
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
|
||||
init {
|
||||
@@ -48,22 +58,41 @@ class RemoteListViewModel(
|
||||
loadFilter()
|
||||
}
|
||||
|
||||
fun loadList(append: Boolean) {
|
||||
override fun onRefresh() {
|
||||
loadList(append = false)
|
||||
}
|
||||
|
||||
override fun onRetry() {
|
||||
loadList(append = !mangaList.value.isNullOrEmpty())
|
||||
}
|
||||
|
||||
fun loadNextPage() {
|
||||
if (hasNextPage.value && listError.value == null) {
|
||||
loadList(append = true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadList(append: Boolean) {
|
||||
if (loadingJob?.isActive == true) {
|
||||
return
|
||||
}
|
||||
loadingJob = launchLoadingJob(Dispatchers.Default) {
|
||||
val list = repository.getList(
|
||||
offset = if (append) mangaList.value.size else 0,
|
||||
sortOrder = appliedFilter?.sortOrder,
|
||||
tag = appliedFilter?.tag
|
||||
)
|
||||
if (!append) {
|
||||
mangaList.value = list
|
||||
} else if (list.isNotEmpty()) {
|
||||
mangaList.value += list
|
||||
try {
|
||||
listError.value = null
|
||||
val list = repository.getList(
|
||||
offset = if (append) mangaList.value?.size ?: 0 else 0,
|
||||
sortOrder = appliedFilter?.sortOrder,
|
||||
tag = appliedFilter?.tag
|
||||
)
|
||||
if (!append) {
|
||||
mangaList.value = list
|
||||
} else if (list.isNotEmpty()) {
|
||||
mangaList.value = mangaList.value?.plus(list) ?: list
|
||||
}
|
||||
hasNextPage.value = list.isNotEmpty()
|
||||
} catch (e: Throwable) {
|
||||
listError.value = e
|
||||
}
|
||||
hasNextPage.value = list.isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ class MangaSearchSheet : MangaListSheet() {
|
||||
}
|
||||
|
||||
override fun onScrolledToEnd() {
|
||||
viewModel.loadList(append = true)
|
||||
viewModel.loadNextPage()
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -39,6 +39,7 @@ class MangaSearchSheet : MangaListSheet() {
|
||||
|
||||
private const val TAG = "MangaSearchSheet"
|
||||
|
||||
@Deprecated("Not ready for use")
|
||||
fun show(fm: FragmentManager, source: MangaSource, query: String) {
|
||||
MangaSearchSheet().withArgs(2) {
|
||||
putParcelable(ARG_SOURCE, source)
|
||||
|
||||
@@ -17,13 +17,8 @@ class SearchFragment : MangaListFragment() {
|
||||
private val query by stringArgument(ARG_QUERY)
|
||||
private val source by parcelableArgument<MangaSource>(ARG_SOURCE)
|
||||
|
||||
override fun onRefresh() {
|
||||
super.onRefresh()
|
||||
viewModel.loadList(append = false)
|
||||
}
|
||||
|
||||
override fun onScrolledToEnd() {
|
||||
viewModel.loadList(append = true)
|
||||
viewModel.loadNextPage()
|
||||
}
|
||||
|
||||
override fun getTitle() = query
|
||||
|
||||
@@ -6,17 +6,14 @@ 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 kotlinx.coroutines.flow.onStart
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
||||
import org.koitharu.kotatsu.list.ui.model.IndeterminateProgress
|
||||
import org.koitharu.kotatsu.list.ui.model.toGridModel
|
||||
import org.koitharu.kotatsu.list.ui.model.toListDetailedModel
|
||||
import org.koitharu.kotatsu.list.ui.model.toListModel
|
||||
import org.koitharu.kotatsu.list.ui.model.*
|
||||
import java.util.*
|
||||
|
||||
class SearchViewModel(
|
||||
private val repository: MangaRepository,
|
||||
@@ -24,41 +21,73 @@ class SearchViewModel(
|
||||
settings: AppSettings
|
||||
) : MangaListViewModel(settings) {
|
||||
|
||||
private val mangaList = MutableStateFlow<List<Manga>>(emptyList())
|
||||
private val mangaList = MutableStateFlow<List<Manga>?>(null)
|
||||
private val hasNextPage = MutableStateFlow(false)
|
||||
private val listError = MutableStateFlow<Throwable?>(null)
|
||||
private var loadingJob: Job? = null
|
||||
|
||||
override val content = combine(mangaList.drop(1), createListModeFlow()) { list, mode ->
|
||||
when (mode) {
|
||||
ListMode.LIST -> list.map { it.toListModel() }
|
||||
ListMode.DETAILED_LIST -> list.map { it.toListDetailedModel() }
|
||||
ListMode.GRID -> list.map { it.toGridModel() }
|
||||
override val content = combine(
|
||||
mangaList,
|
||||
createListModeFlow(),
|
||||
listError,
|
||||
hasNextPage
|
||||
) { list, mode, error, hasNext ->
|
||||
when {
|
||||
list.isNullOrEmpty() && error != null -> listOf(error.toErrorState(canRetry = true))
|
||||
list == null -> listOf(LoadingState)
|
||||
list.isEmpty() -> listOf(EmptyState(R.string.nothing_found))
|
||||
else -> {
|
||||
val result = ArrayList<ListModel>(list.size + 1)
|
||||
list.toUi(result, mode)
|
||||
when {
|
||||
error != null -> result += error.toErrorFooter()
|
||||
hasNext -> result += LoadingFooter
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
}.onEach {
|
||||
isEmptyState.postValue(it.isEmpty())
|
||||
}.combine(hasNextPage) { list, isHasNextPage ->
|
||||
if (isHasNextPage && list.isNotEmpty()) list + IndeterminateProgress else list
|
||||
}.onStart {
|
||||
emit(listOf(LoadingState))
|
||||
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
|
||||
init {
|
||||
loadList(append = false)
|
||||
}
|
||||
|
||||
fun loadList(append: Boolean) {
|
||||
override fun onRefresh() {
|
||||
loadList(append = false)
|
||||
}
|
||||
|
||||
override fun onRetry() {
|
||||
loadList(append = !mangaList.value.isNullOrEmpty())
|
||||
}
|
||||
|
||||
fun loadNextPage() {
|
||||
if (hasNextPage.value && listError.value == null) {
|
||||
loadList(append = true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadList(append: Boolean) {
|
||||
if (loadingJob?.isActive == true) {
|
||||
return
|
||||
}
|
||||
loadingJob = launchLoadingJob(Dispatchers.Default) {
|
||||
val list = repository.getList(
|
||||
offset = if (append) mangaList.value.size else 0,
|
||||
query = query
|
||||
)
|
||||
if (!append) {
|
||||
mangaList.value = list
|
||||
} else if (list.isNotEmpty()) {
|
||||
mangaList.value += list
|
||||
try {
|
||||
listError.value = null
|
||||
val list = repository.getList(
|
||||
offset = if (append) mangaList.value?.size ?: 0 else 0,
|
||||
query = query
|
||||
)
|
||||
if (!append) {
|
||||
mangaList.value = list
|
||||
} else if (list.isNotEmpty()) {
|
||||
mangaList.value = mangaList.value?.plus(list) ?: list
|
||||
}
|
||||
hasNextPage.value = list.isNotEmpty()
|
||||
} catch (e: Throwable) {
|
||||
listError.value = e
|
||||
}
|
||||
hasNextPage.value = list.isNotEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,11 +15,6 @@ class GlobalSearchFragment : MangaListFragment() {
|
||||
|
||||
private val query by stringArgument(ARG_QUERY)
|
||||
|
||||
override fun onRefresh() {
|
||||
super.onRefresh()
|
||||
viewModel.onRefresh()
|
||||
}
|
||||
|
||||
override fun onScrolledToEnd() = Unit
|
||||
|
||||
override fun getTitle(): CharSequence? {
|
||||
|
||||
@@ -6,16 +6,14 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
||||
import org.koitharu.kotatsu.list.ui.model.IndeterminateProgress
|
||||
import org.koitharu.kotatsu.list.ui.model.toGridModel
|
||||
import org.koitharu.kotatsu.list.ui.model.toListDetailedModel
|
||||
import org.koitharu.kotatsu.list.ui.model.toListModel
|
||||
import org.koitharu.kotatsu.list.ui.model.*
|
||||
import org.koitharu.kotatsu.search.domain.MangaSearchRepository
|
||||
import org.koitharu.kotatsu.utils.ext.onFirst
|
||||
import java.util.*
|
||||
|
||||
class GlobalSearchViewModel(
|
||||
private val query: String,
|
||||
@@ -23,48 +21,63 @@ class GlobalSearchViewModel(
|
||||
settings: AppSettings
|
||||
) : MangaListViewModel(settings) {
|
||||
|
||||
private val mangaList = MutableStateFlow<List<Manga>>(emptyList())
|
||||
private val mangaList = MutableStateFlow<List<Manga>?>(null)
|
||||
private val hasNextPage = MutableStateFlow(false)
|
||||
private val listError = MutableStateFlow<Throwable?>(null)
|
||||
private var searchJob: Job? = null
|
||||
|
||||
override val content = combine(mangaList.drop(1), createListModeFlow()) { list, mode ->
|
||||
when (mode) {
|
||||
ListMode.LIST -> list.map { it.toListModel() }
|
||||
ListMode.DETAILED_LIST -> list.map { it.toListDetailedModel() }
|
||||
ListMode.GRID -> list.map { it.toGridModel() }
|
||||
override val content = combine(
|
||||
mangaList,
|
||||
createListModeFlow(),
|
||||
listError,
|
||||
hasNextPage
|
||||
) { list, mode, error, hasNext ->
|
||||
when {
|
||||
list.isNullOrEmpty() && error != null -> listOf(error.toErrorState(canRetry = true))
|
||||
list == null -> listOf(LoadingState)
|
||||
list.isEmpty() -> listOf(EmptyState(R.string.nothing_found))
|
||||
else -> {
|
||||
val result = ArrayList<ListModel>(list.size + 1)
|
||||
list.toUi(result, mode)
|
||||
when {
|
||||
error != null -> result += error.toErrorFooter()
|
||||
hasNext -> result += LoadingFooter
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
}.combine(hasNextPage) { list, isHasNextPage ->
|
||||
if (isHasNextPage && list.isNotEmpty()) list + IndeterminateProgress else list
|
||||
}.onStart {
|
||||
emit(listOf(LoadingState))
|
||||
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
|
||||
init {
|
||||
onRefresh()
|
||||
}
|
||||
|
||||
fun onRefresh() {
|
||||
override fun onRetry() {
|
||||
onRefresh()
|
||||
}
|
||||
|
||||
override fun onRefresh() {
|
||||
searchJob?.cancel()
|
||||
searchJob = repository.globalSearch(query)
|
||||
.flowOn(Dispatchers.Default)
|
||||
.catch { e ->
|
||||
onError.postCall(e)
|
||||
listError.value = e
|
||||
isLoading.postValue(false)
|
||||
hasNextPage.value = false
|
||||
}.filterNot { x -> x.isEmpty() }
|
||||
.onStart {
|
||||
listError.value = null
|
||||
isLoading.postValue(true)
|
||||
hasNextPage.value = true
|
||||
}.onEmpty {
|
||||
mangaList.value = emptyList()
|
||||
isEmptyState.postValue(true)
|
||||
isLoading.postValue(false)
|
||||
}.onCompletion {
|
||||
isLoading.postValue(false)
|
||||
hasNextPage.value = false
|
||||
}.onFirst {
|
||||
isEmptyState.postValue(false)
|
||||
hasNextPage.value = true
|
||||
isLoading.value = false
|
||||
isLoading.postValue(false)
|
||||
}.onEach {
|
||||
mangaList.value += it
|
||||
mangaList.value = mangaList.value?.plus(it) ?: it
|
||||
}.launchIn(viewModelScope + Dispatchers.Default)
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.databinding.FragmentFeedBinding
|
||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.tracker.ui.adapter.FeedAdapter
|
||||
import org.koitharu.kotatsu.tracker.work.TrackWorker
|
||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
@@ -79,7 +80,7 @@ class FeedFragment : BaseFragment<FragmentFeedBinding>(), PaginationScrollListen
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun onListChanged(list: List<Any>) {
|
||||
private fun onListChanged(list: List<ListModel>) {
|
||||
feedAdapter?.items = list
|
||||
}
|
||||
|
||||
|
||||
@@ -8,10 +8,12 @@ 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.filterNotNull
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.model.TrackingLogItem
|
||||
import org.koitharu.kotatsu.list.ui.model.IndeterminateProgress
|
||||
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
|
||||
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||
import org.koitharu.kotatsu.tracker.ui.model.toFeedItem
|
||||
import org.koitharu.kotatsu.utils.ext.mapItems
|
||||
@@ -21,18 +23,20 @@ class FeedViewModel(
|
||||
private val repository: TrackingRepository
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val logList = MutableStateFlow<List<TrackingLogItem>>(emptyList())
|
||||
private val logList = MutableStateFlow<List<TrackingLogItem>?>(null)
|
||||
private val hasNextPage = MutableStateFlow(false)
|
||||
private var loadingJob: Job? = null
|
||||
|
||||
val isEmptyState = MutableLiveData(false)
|
||||
val content = combine(
|
||||
logList.drop(1).mapItems {
|
||||
logList.filterNotNull().mapItems {
|
||||
it.toFeedItem(context.resources)
|
||||
},
|
||||
hasNextPage
|
||||
) { list, isHasNextPage ->
|
||||
if (isHasNextPage && list.isNotEmpty()) list + IndeterminateProgress else list
|
||||
if (isHasNextPage && list.isNotEmpty()) list + LoadingFooter else list
|
||||
}.onStart {
|
||||
emit(listOf(LoadingState))
|
||||
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
|
||||
init {
|
||||
@@ -44,13 +48,13 @@ class FeedViewModel(
|
||||
return
|
||||
}
|
||||
loadingJob = launchLoadingJob(Dispatchers.Default) {
|
||||
val offset = if (append) logList.value.size else 0
|
||||
val offset = if (append) logList.value?.size ?: 0 else 0
|
||||
val list = repository.getTrackingLog(offset, 20)
|
||||
if (!append) {
|
||||
logList.value = list
|
||||
isEmptyState.postValue(list.isEmpty())
|
||||
} else if (list.isNotEmpty()) {
|
||||
logList.value += list
|
||||
logList.value = logList.value?.plus(list) ?: list
|
||||
}
|
||||
hasNextPage.value = list.isNotEmpty()
|
||||
}
|
||||
|
||||
@@ -6,8 +6,9 @@ import coil.ImageLoader
|
||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||
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.list.ui.adapter.*
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
|
||||
import org.koitharu.kotatsu.tracker.ui.model.FeedItem
|
||||
import kotlin.jvm.internal.Intrinsics
|
||||
|
||||
@@ -15,27 +16,31 @@ class FeedAdapter(
|
||||
coil: ImageLoader,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
clickListener: OnListItemClickListener<Manga>
|
||||
) : AsyncListDifferDelegationAdapter<Any>(DiffCallback()) {
|
||||
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()) {
|
||||
|
||||
init {
|
||||
delegatesManager
|
||||
.addDelegate(ITEM_TYPE_FEED, feedItemAD(coil, lifecycleOwner, clickListener))
|
||||
.addDelegate(ITEM_TYPE_PROGRESS, indeterminateProgressAD())
|
||||
.addDelegate(ITEM_TYPE_LOADING_FOOTER, loadingFooterAD())
|
||||
.addDelegate(ITEM_TYPE_LOADING_STATE, loadingStateAD())
|
||||
.addDelegate(ITEM_TYPE_ERROR_FOOTER, errorFooterAD {})
|
||||
.addDelegate(ITEM_TYPE_ERROR_STATE, errorStateListAD {})
|
||||
.addDelegate(ITEM_TYPE_EMPTY, emptyStateListAD())
|
||||
}
|
||||
|
||||
private class DiffCallback : DiffUtil.ItemCallback<Any>() {
|
||||
private class DiffCallback : DiffUtil.ItemCallback<ListModel>() {
|
||||
|
||||
override fun areItemsTheSame(oldItem: Any, newItem: Any) = when {
|
||||
override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel) = when {
|
||||
oldItem is FeedItem && newItem is FeedItem -> {
|
||||
oldItem.id == newItem.id
|
||||
}
|
||||
oldItem == IndeterminateProgress && newItem == IndeterminateProgress -> {
|
||||
oldItem == LoadingFooter && newItem == LoadingFooter -> {
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: Any, newItem: Any): Boolean {
|
||||
override fun areContentsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
|
||||
return Intrinsics.areEqual(oldItem, newItem)
|
||||
}
|
||||
}
|
||||
@@ -43,6 +48,10 @@ class FeedAdapter(
|
||||
companion object {
|
||||
|
||||
const val ITEM_TYPE_FEED = 0
|
||||
const val ITEM_TYPE_PROGRESS = 1
|
||||
const val ITEM_TYPE_LOADING_FOOTER = 1
|
||||
const val ITEM_TYPE_LOADING_STATE = 2
|
||||
const val ITEM_TYPE_ERROR_STATE = 3
|
||||
const val ITEM_TYPE_ERROR_FOOTER = 4
|
||||
const val ITEM_TYPE_EMPTY = 5
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.databinding.ItemTracklogBinding
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.tracker.ui.model.FeedItem
|
||||
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.utils.ext.newImageRequest
|
||||
@@ -16,7 +17,7 @@ fun feedItemAD(
|
||||
coil: ImageLoader,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
clickListener: OnListItemClickListener<Manga>
|
||||
) = adapterDelegateViewBinding<FeedItem, Any, ItemTracklogBinding>(
|
||||
) = adapterDelegateViewBinding<FeedItem, ListModel, ItemTracklogBinding>(
|
||||
{ inflater, parent -> ItemTracklogBinding.inflate(inflater, parent, false) }
|
||||
) {
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.koitharu.kotatsu.tracker.ui.model
|
||||
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
data class FeedItem(
|
||||
val id: Long,
|
||||
@@ -9,4 +10,4 @@ data class FeedItem(
|
||||
val subtitle: String,
|
||||
val chapters: CharSequence,
|
||||
val manga: Manga
|
||||
)
|
||||
) : ListModel
|
||||
@@ -43,7 +43,7 @@ fun Throwable.getDisplayMessage(resources: Resources) = when (this) {
|
||||
is EmptyHistoryException -> resources.getString(R.string.history_is_empty)
|
||||
is SocketTimeoutException -> resources.getString(R.string.network_error)
|
||||
is WrongPasswordException -> resources.getString(R.string.wrong_password)
|
||||
else -> message ?: resources.getString(R.string.error_occurred)
|
||||
else -> localizedMessage ?: resources.getString(R.string.error_occurred)
|
||||
}
|
||||
|
||||
inline fun <T> measured(tag: String, block: () -> T): T {
|
||||
|
||||
12
app/src/main/res/drawable/ic_alert_outline.xml
Normal file
12
app/src/main/res/drawable/ic_alert_outline.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M12,2L1,21H23M12,6L19.53,19H4.47M11,10V14H13V10M11,16V18H13V16" />
|
||||
</vector>
|
||||
@@ -8,69 +8,35 @@
|
||||
android:animateLayoutChanges="true"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<FrameLayout
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipeRefreshLayout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1">
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipeRefreshLayout"
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:scrollbars="vertical"
|
||||
app:fastScrollEnabled="true"
|
||||
app:fastScrollHorizontalThumbDrawable="@drawable/list_thumb"
|
||||
app:fastScrollHorizontalTrackDrawable="@drawable/list_track"
|
||||
app:fastScrollVerticalThumbDrawable="@drawable/list_thumb"
|
||||
app:fastScrollVerticalTrackDrawable="@drawable/list_track"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/item_manga_list" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:scrollbars="vertical"
|
||||
app:fastScrollEnabled="true"
|
||||
app:fastScrollHorizontalThumbDrawable="@drawable/list_thumb"
|
||||
app:fastScrollHorizontalTrackDrawable="@drawable/list_track"
|
||||
app:fastScrollVerticalThumbDrawable="@drawable/list_thumb"
|
||||
app:fastScrollVerticalTrackDrawable="@drawable/list_track"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/item_manga_list" />
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_holder"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_holder"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
tools:text="@tools:sample/lorem[3]" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:indeterminate="true" />
|
||||
|
||||
</FrameLayout>
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/divider_filter"
|
||||
android:layout_width="1dp"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?dividerVertical"
|
||||
android:visibility="gone" />
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView_filter"
|
||||
@@ -82,6 +48,7 @@
|
||||
android:scrollbars="vertical"
|
||||
android:visibility="gone"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/item_category_checkable" />
|
||||
tools:listitem="@layout/item_category_checkable"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -11,13 +11,12 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_margin="40dp"
|
||||
android:indeterminate="true"
|
||||
app:indicatorColor="?colorAccent" />
|
||||
android:indeterminate="true" />
|
||||
|
||||
</FrameLayout>
|
||||
@@ -12,61 +12,26 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<FrameLayout
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipeRefreshLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipeRefreshLayout"
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:scrollbars="vertical"
|
||||
app:fastScrollEnabled="true"
|
||||
app:fastScrollHorizontalThumbDrawable="@drawable/list_thumb"
|
||||
app:fastScrollHorizontalTrackDrawable="@drawable/list_track"
|
||||
app:fastScrollVerticalThumbDrawable="@drawable/list_thumb"
|
||||
app:fastScrollVerticalTrackDrawable="@drawable/list_track"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/item_manga_list" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:scrollbars="vertical"
|
||||
app:fastScrollEnabled="true"
|
||||
app:fastScrollHorizontalThumbDrawable="@drawable/list_thumb"
|
||||
app:fastScrollHorizontalTrackDrawable="@drawable/list_track"
|
||||
app:fastScrollVerticalThumbDrawable="@drawable/list_thumb"
|
||||
app:fastScrollVerticalTrackDrawable="@drawable/list_track"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/item_manga_list" />
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_holder"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_holder"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
tools:text="@tools:sample/lorem[3]" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:indeterminate="true" />
|
||||
|
||||
</FrameLayout>
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView_filter"
|
||||
|
||||
10
app/src/main/res/layout/item_empty_state.xml
Normal file
10
app/src/main/res/layout/item_empty_state.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
tools:text="@tools:sample/lorem[3]" />
|
||||
42
app/src/main/res/layout/item_error_footer.xml
Normal file
42
app/src/main/res/layout/item_error_footer.xml
Normal file
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/list_footer_height"
|
||||
android:background="?selectableItemBackground"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:padding="4dp">
|
||||
|
||||
<ImageView
|
||||
android:contentDescription="@null"
|
||||
android:id="@+id/imageView_icon"
|
||||
tools:src="@drawable/ic_alert_outline"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
tools:text="@string/error_occurred" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/tap_to_try_again"
|
||||
android:textAppearance="?android:textAppearanceSmall"
|
||||
android:textColor="?android:textColorSecondary" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
31
app/src/main/res/layout/item_error_state.xml
Normal file
31
app/src/main/res/layout/item_error_state.xml
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_error"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:drawablePadding="12dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
app:drawableTopCompat="@drawable/ic_error_large"
|
||||
tools:text="@tools:sample/lorem[6]" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_retry"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/try_again" />
|
||||
|
||||
</LinearLayout>
|
||||
16
app/src/main/res/layout/item_loading_footer.xml
Normal file
16
app/src/main/res/layout/item_loading_footer.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/list_footer_height"
|
||||
android:padding="6dp">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:indeterminate="true" />
|
||||
|
||||
</FrameLayout>
|
||||
@@ -2,14 +2,13 @@
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="4dp">
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
style="@style/Widget.AppCompat.ProgressBar"
|
||||
style="?android:progressBarStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center" />
|
||||
android:layout_gravity="center"
|
||||
android:indeterminate="true" />
|
||||
|
||||
</FrameLayout>
|
||||
@@ -185,4 +185,5 @@
|
||||
<string name="long_ago">Давно</string>
|
||||
<string name="group">Группировать</string>
|
||||
<string name="today">Сегодня</string>
|
||||
<string name="tap_to_try_again">Tap to try again</string>
|
||||
</resources>
|
||||
@@ -7,4 +7,5 @@
|
||||
<dimen name="preferred_grid_width">120dp</dimen>
|
||||
<dimen name="header_height">34dp</dimen>
|
||||
<dimen name="elevation_large">16dp</dimen>
|
||||
<dimen name="list_footer_height">48dp</dimen>
|
||||
</resources>
|
||||
@@ -187,4 +187,5 @@
|
||||
<string name="long_ago">Long ago</string>
|
||||
<string name="group">Group</string>
|
||||
<string name="today">Today</string>
|
||||
<string name="tap_to_try_again">Tap to try again</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user