Fully manga list fragments to AdapterDelegates and mvvm

This commit is contained in:
Koitharu
2020-11-20 20:07:57 +02:00
parent 7e76e10591
commit 971f708e45
18 changed files with 203 additions and 135 deletions

View File

@@ -1,5 +1,6 @@
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) :
@@ -10,7 +11,7 @@ class PaginationScrollListener(offset: Int, private val callback: Callback) :
override fun onScrolledToStart(recyclerView: RecyclerView) = Unit override fun onScrolledToStart(recyclerView: RecyclerView) = Unit
override fun onScrolledToEnd(recyclerView: RecyclerView) { override fun onScrolledToEnd(recyclerView: RecyclerView) {
val total = callback.getItemsCount() val total = (recyclerView.layoutManager as? LinearLayoutManager)?.itemCount ?: return
if (total > lastTotalCount) { if (total > lastTotalCount) {
lastTotalCount = total lastTotalCount = total
callback.onRequestMoreItems(total) callback.onRequestMoreItems(total)
@@ -27,6 +28,7 @@ class PaginationScrollListener(offset: Int, private val callback: Callback) :
fun onRequestMoreItems(offset: Int) fun onRequestMoreItems(offset: Int)
fun getItemsCount(): Int @Deprecated("Not in use")
fun getItemsCount(): Int = 0
} }
} }

View File

@@ -22,7 +22,7 @@ class AppSettings private constructor(private val prefs: SharedPreferences) :
PreferenceManager.getDefaultSharedPreferences(context) PreferenceManager.getDefaultSharedPreferences(context)
) )
var listMode by IntEnumPreferenceDelegate( var listMode by EnumPreferenceDelegate(
ListMode::class.java, ListMode::class.java,
KEY_LIST_MODE, KEY_LIST_MODE,
ListMode.DETAILED_LIST ListMode.DETAILED_LIST
@@ -41,7 +41,7 @@ class AppSettings private constructor(private val prefs: SharedPreferences) :
val isAmoledTheme by BoolPreferenceDelegate(KEY_THEME_AMOLED, defaultValue = false) val isAmoledTheme by BoolPreferenceDelegate(KEY_THEME_AMOLED, defaultValue = false)
val gridSize by IntPreferenceDelegate(KEY_GRID_SIZE, defaultValue = 100) var gridSize by IntPreferenceDelegate(KEY_GRID_SIZE, defaultValue = 100)
val readerPageSwitch by StringSetPreferenceDelegate( val readerPageSwitch by StringSetPreferenceDelegate(
KEY_READER_SWITCHERS, KEY_READER_SWITCHERS,
@@ -138,7 +138,7 @@ class AppSettings private constructor(private val prefs: SharedPreferences) :
const val TRACK_HISTORY = "history" const val TRACK_HISTORY = "history"
const val TRACK_FAVOURITES = "favourites" const val TRACK_FAVOURITES = "favourites"
const val KEY_LIST_MODE = "list_mode" const val KEY_LIST_MODE = "list_mode_2"
const val KEY_APP_SECTION = "app_section" const val KEY_APP_SECTION = "app_section"
const val KEY_THEME = "theme" const val KEY_THEME = "theme"
const val KEY_THEME_AMOLED = "amoled_theme" const val KEY_THEME_AMOLED = "amoled_theme"

View File

@@ -4,6 +4,8 @@ import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.core.prefs.ListMode
@@ -12,6 +14,7 @@ import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.toGridModel import org.koitharu.kotatsu.list.ui.model.toGridModel
import org.koitharu.kotatsu.list.ui.model.toListDetailedModel import org.koitharu.kotatsu.list.ui.model.toListDetailedModel
import org.koitharu.kotatsu.list.ui.model.toListModel import org.koitharu.kotatsu.list.ui.model.toListModel
import org.koitharu.kotatsu.utils.ext.onFirst
class FavouritesListViewModel( class FavouritesListViewModel(
private val categoryId: Long, private val categoryId: Long,
@@ -28,6 +31,12 @@ class FavouritesListViewModel(
ListMode.DETAILED_LIST -> list.map { it.toListDetailedModel() } ListMode.DETAILED_LIST -> list.map { it.toListDetailedModel() }
ListMode.GRID -> list.map { it.toGridModel() } ListMode.GRID -> list.map { it.toGridModel() }
} }
}.onEach {
isEmptyState.postValue(it.isEmpty())
}.onStart {
isLoading.postValue(true)
}.onFirst {
isLoading.postValue(false)
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default) }.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
fun removeFromFavourites(manga: Manga) { fun removeFromFavourites(manga: Manga) {

View File

@@ -6,6 +6,8 @@ import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.core.prefs.ListMode
@@ -16,6 +18,7 @@ import org.koitharu.kotatsu.list.ui.model.toListDetailedModel
import org.koitharu.kotatsu.list.ui.model.toListModel import org.koitharu.kotatsu.list.ui.model.toListModel
import org.koitharu.kotatsu.utils.MangaShortcut import org.koitharu.kotatsu.utils.MangaShortcut
import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.onFirst
class HistoryListViewModel( class HistoryListViewModel(
private val repository: HistoryRepository, private val repository: HistoryRepository,
@@ -34,6 +37,12 @@ class HistoryListViewModel(
ListMode.DETAILED_LIST -> list.map { it.toListDetailedModel() } ListMode.DETAILED_LIST -> list.map { it.toListDetailedModel() }
ListMode.GRID -> list.map { it.toGridModel() } ListMode.GRID -> list.map { it.toGridModel() }
} }
}.onEach {
isEmptyState.postValue(it.isEmpty())
}.onStart {
isLoading.postValue(true)
}.onFirst {
isLoading.postValue(false)
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default) }.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
fun clearHistory() { fun clearHistory() {

View File

@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.list.ui
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.SeekBar
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import kotlinx.android.synthetic.main.dialog_list_mode.* import kotlinx.android.synthetic.main.dialog_list_mode.*
@@ -11,19 +12,23 @@ import org.koitharu.kotatsu.base.ui.AlertDialogFragment
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.core.prefs.ListMode
class ListModeSelectDialog : AlertDialogFragment(R.layout.dialog_list_mode), View.OnClickListener { class ListModeSelectDialog : AlertDialogFragment(R.layout.dialog_list_mode), View.OnClickListener,
SeekBar.OnSeekBarChangeListener {
private val settings by inject<AppSettings>() private val settings by inject<AppSettings>()
private lateinit var mode: ListMode private var mode: ListMode = ListMode.GRID
private var pendingGridSize: Int = 100
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
mode = settings.listMode mode = settings.listMode
pendingGridSize = settings.gridSize
} }
override fun onBuildDialog(builder: AlertDialog.Builder) { override fun onBuildDialog(builder: AlertDialog.Builder) {
builder.setTitle(R.string.list_mode) builder.setTitle(R.string.list_mode)
.setPositiveButton(R.string.done, null)
.setCancelable(true) .setCancelable(true)
} }
@@ -33,22 +38,33 @@ class ListModeSelectDialog : AlertDialogFragment(R.layout.dialog_list_mode), Vie
button_list_detailed.isChecked = mode == ListMode.DETAILED_LIST button_list_detailed.isChecked = mode == ListMode.DETAILED_LIST
button_grid.isChecked = mode == ListMode.GRID button_grid.isChecked = mode == ListMode.GRID
button_ok.setOnClickListener(this) with(seekbar_grid) {
progress = pendingGridSize - 50
setOnSeekBarChangeListener(this@ListModeSelectDialog)
}
button_list.setOnClickListener(this) button_list.setOnClickListener(this)
button_grid.setOnClickListener(this) button_grid.setOnClickListener(this)
button_list_detailed.setOnClickListener(this) button_list_detailed.setOnClickListener(this)
} }
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
pendingGridSize = progress + 50
}
override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit
override fun onStopTrackingTouch(seekBar: SeekBar?) {
settings.gridSize = pendingGridSize
}
override fun onClick(v: View) { override fun onClick(v: View) {
when (v.id) { when (v.id) {
R.id.button_ok -> {
settings.listMode = mode
dismiss()
}
R.id.button_list -> mode = ListMode.LIST R.id.button_list -> mode = ListMode.LIST
R.id.button_list_detailed -> mode = ListMode.DETAILED_LIST R.id.button_list_detailed -> mode = ListMode.DETAILED_LIST
R.id.button_grid -> mode = ListMode.GRID R.id.button_grid -> mode = ListMode.GRID
} }
settings.listMode = mode
} }
companion object { companion object {

View File

@@ -32,8 +32,10 @@ import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter
import org.koitharu.kotatsu.list.ui.filter.FilterAdapter import org.koitharu.kotatsu.list.ui.filter.FilterAdapter
import org.koitharu.kotatsu.list.ui.filter.OnFilterChangedListener import org.koitharu.kotatsu.list.ui.filter.OnFilterChangedListener
import org.koitharu.kotatsu.utils.UiUtils import org.koitharu.kotatsu.utils.ext.clearItemDecorations
import org.koitharu.kotatsu.utils.ext.* 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(R.layout.fragment_list), abstract class MangaListFragment : BaseFragment(R.layout.fragment_list),
PaginationScrollListener.Callback, OnListItemClickListener<Manga>, OnFilterChangedListener, PaginationScrollListener.Callback, OnListItemClickListener<Manga>, OnFilterChangedListener,
@@ -41,7 +43,8 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list),
private var adapter: MangaListAdapter? = null private var adapter: MangaListAdapter? = null
private var paginationListener: PaginationScrollListener? = null private var paginationListener: PaginationScrollListener? = null
private val spanResolver: MangaListSpanResolver? = null private val spanResolver = MangaListSpanResolver()
private val spanSizeLookup = SpanSizeLookup()
protected var isSwipeRefreshEnabled = true protected var isSwipeRefreshEnabled = true
protected abstract val viewModel: MangaListViewModel protected abstract val viewModel: MangaListViewModel
@@ -70,11 +73,13 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list),
viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged) viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged)
viewModel.listMode.observe(viewLifecycleOwner, ::onListModeChanged) viewModel.listMode.observe(viewLifecycleOwner, ::onListModeChanged)
viewModel.gridScale.observe(viewLifecycleOwner, ::onGridScaleChanged) viewModel.gridScale.observe(viewLifecycleOwner, ::onGridScaleChanged)
viewModel.isEmptyState.observe(viewLifecycleOwner, ::onEmptyStateChanged)
} }
override fun onDestroyView() { override fun onDestroyView() {
adapter = null adapter = null
paginationListener = null paginationListener = null
spanSizeLookup.invalidateCache()
super.onDestroyView() super.onDestroyView()
} }
@@ -126,24 +131,15 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list),
} }
private fun onListChanged(list: List<Any>) { private fun onListChanged(list: List<Any>) {
spanSizeLookup.invalidateCache()
adapter?.items = 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) {
if (e is CloudFlareProtectedException) { if (e is CloudFlareProtectedException) {
CloudFlareDialog.newInstance(e.url).show(childFragmentManager, CloudFlareDialog.TAG) CloudFlareDialog.newInstance(e.url).show(childFragmentManager, CloudFlareDialog.TAG)
} }
if (recyclerView.hasItems) { if (viewModel.isEmptyState.value == true) {
Snackbar.make(recyclerView, e.getDisplayMessage(resources), Snackbar.LENGTH_SHORT)
.show()
} else {
textView_holder.text = e.getDisplayMessage(resources) textView_holder.text = e.getDisplayMessage(resources)
textView_holder.setCompoundDrawablesRelativeWithIntrinsicBounds( textView_holder.setCompoundDrawablesRelativeWithIntrinsicBounds(
0, 0,
@@ -152,21 +148,29 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list),
0 0
) )
layout_holder.isVisible = true layout_holder.isVisible = true
} else {
Snackbar.make(recyclerView, e.getDisplayMessage(resources), Snackbar.LENGTH_SHORT)
.show()
} }
} }
@CallSuper @CallSuper
protected open fun onLoadingStateChanged(isLoading: Boolean) { protected open fun onLoadingStateChanged(isLoading: Boolean) {
val hasItems = recyclerView.hasItems val hasItems = recyclerView.hasItems
progressBar.isVisible = isLoading && !hasItems progressBar.isVisible = isLoading && !hasItems && viewModel.isEmptyState.value != true
swipeRefreshLayout.isEnabled = isSwipeRefreshEnabled && !progressBar.isVisible swipeRefreshLayout.isEnabled = isSwipeRefreshEnabled && !progressBar.isVisible
if (isLoading) { if (!isLoading) {
layout_holder.isVisible = false
} else {
swipeRefreshLayout.isRefreshing = false swipeRefreshLayout.isRefreshing = false
} }
} }
private fun onEmptyStateChanged(isEmpty: Boolean) {
if (isEmpty) {
setUpEmptyListHolder()
}
layout_holder.isVisible = isEmpty
}
protected fun onInitFilter(config: MangaFilterConfig) { protected fun onInitFilter(config: MangaFilterConfig) {
recyclerView_filter.adapter = FilterAdapter( recyclerView_filter.adapter = FilterAdapter(
sortOrders = config.sortOrders, sortOrders = config.sortOrders,
@@ -198,12 +202,15 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list),
} }
private fun onGridScaleChanged(scale: Float) { private fun onGridScaleChanged(scale: Float) {
UiUtils.SpanCountResolver.update(recyclerView) spanSizeLookup.invalidateCache()
spanResolver.setGridSize(scale, recyclerView)
} }
private fun onListModeChanged(mode: ListMode) { private fun onListModeChanged(mode: ListMode) {
spanSizeLookup.invalidateCache()
with(recyclerView) { with(recyclerView) {
clearItemDecorations() clearItemDecorations()
removeOnLayoutChangeListener(spanResolver)
when (mode) { when (mode) {
ListMode.LIST -> { ListMode.LIST -> {
layoutManager = LinearLayoutManager(context) layoutManager = LinearLayoutManager(context)
@@ -216,52 +223,27 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list),
} }
ListMode.DETAILED_LIST -> { ListMode.DETAILED_LIST -> {
layoutManager = LinearLayoutManager(context) layoutManager = LinearLayoutManager(context)
}
ListMode.GRID -> {
layoutManager = GridLayoutManager(context, 3)
addItemDecoration( addItemDecoration(
SpacingItemDecoration( SpacingItemDecoration(
resources.getDimensionPixelOffset(R.dimen.grid_spacing) resources.getDimensionPixelOffset(R.dimen.grid_spacing)
) )
) )
} }
} ListMode.GRID -> {
} layoutManager = GridLayoutManager(context, spanResolver.spanCount).also {
} it.spanSizeLookup = spanSizeLookup
private fun initListMode(mode: ListMode) {
val ctx = context ?: return
recyclerView.layoutManager = null
recyclerView.clearItemDecorations()
recyclerView.removeOnLayoutChangeListener(UiUtils.SpanCountResolver)
recyclerView.layoutManager = when (mode) {
ListMode.GRID -> {
GridLayoutManager(ctx, UiUtils.resolveGridSpanCount(ctx)).apply {
spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int) = if (position < getItemsCount())
1 else this@apply.spanCount
} }
addItemDecoration(
SpacingItemDecoration(
resources.getDimensionPixelOffset(R.dimen.grid_spacing)
)
)
addOnLayoutChangeListener(spanResolver)
} }
} }
else -> LinearLayoutManager(ctx)
} }
recyclerView.addItemDecoration(
when (mode) {
ListMode.LIST -> DividerItemDecoration(ctx, RecyclerView.VERTICAL)
ListMode.DETAILED_LIST,
ListMode.GRID -> SpacingItemDecoration(
resources.getDimensionPixelOffset(R.dimen.grid_spacing)
)
}
)
if (mode == ListMode.GRID) {
recyclerView.addOnLayoutChangeListener(UiUtils.SpanCountResolver)
}
adapter?.notifyDataSetChanged()
} }
override fun getItemsCount() = adapter?.itemCount ?: 0
final override fun isSection(position: Int): Boolean { final override fun isSection(position: Int): Boolean {
return position == 0 || recyclerView_filter.adapter?.run { return position == 0 || recyclerView_filter.adapter?.run {
getItemViewType(position) != getItemViewType(position - 1) getItemViewType(position) != getItemViewType(position - 1)
@@ -279,4 +261,25 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list),
protected open fun onCreatePopupMenu(inflater: MenuInflater, menu: Menu, data: Manga) = Unit protected open fun onCreatePopupMenu(inflater: MenuInflater, menu: Menu, data: Manga) = Unit
protected open fun onPopupMenuItemSelected(item: MenuItem, data: Manga) = false protected open fun onPopupMenuItemSelected(item: MenuItem, data: Manga) = false
private inner class SpanSizeLookup : GridLayoutManager.SpanSizeLookup() {
init {
isSpanIndexCacheEnabled = true
isSpanGroupIndexCacheEnabled = true
}
override fun getSpanSize(position: Int): Int {
val total = (recyclerView.layoutManager as? GridLayoutManager)?.spanCount ?: return 1
return when(adapter?.getItemViewType(position)) {
MangaListAdapter.ITEM_TYPE_PROGRESS -> total
else -> 1
}
}
fun invalidateCache() {
invalidateSpanGroupIndexCache()
invalidateSpanIndexCache()
}
}
} }

View File

@@ -1,26 +1,20 @@
package org.koitharu.kotatsu.list.ui package org.koitharu.kotatsu.list.ui
import android.content.Context
import android.view.View import android.view.View
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.roundToInt import kotlin.math.roundToInt
class MangaListSpanResolver( class MangaListSpanResolver : View.OnLayoutChangeListener {
context: Context,
private val adapter: MangaListAdapter
) : GridLayoutManager.SpanSizeLookup(), View.OnLayoutChangeListener {
private val gridWidth = context.resources.getDimension(R.dimen.preferred_grid_width) var spanCount = 3
private set
private var gridWidth = -1f
private var cellWidth = -1f private var cellWidth = -1f
override fun getSpanSize(position: Int) = when(adapter.getItemViewType(position)) {
else -> 1
}
override fun onLayoutChange( override fun onLayoutChange(
v: View?, v: View?,
left: Int, left: Int,
@@ -36,20 +30,33 @@ class MangaListSpanResolver(
return return
} }
val rv = v as? RecyclerView ?: return val rv = v as? RecyclerView ?: return
if (gridWidth < 0f) {
gridWidth = rv.resources.getDimension(R.dimen.preferred_grid_width)
}
val width = abs(right - left) val width = abs(right - left)
if (width == 0) { if (width == 0) {
return return
} }
(rv.layoutManager as? GridLayoutManager)?.spanCount = resolveGridSpanCount(width) resolveGridSpanCount(width)
(rv.layoutManager as? GridLayoutManager)?.spanCount = spanCount
} }
fun setGridSize(gridSize: Int) { fun setGridSize(scaleFactor: Float, rv: RecyclerView?) {
val scaleFactor = gridSize / 100f if (gridWidth < 0f) {
gridWidth = (rv ?: return).resources.getDimension(R.dimen.preferred_grid_width)
}
cellWidth = gridWidth * scaleFactor cellWidth = gridWidth * scaleFactor
if (rv != null) {
val width = rv.width
if (width != 0) {
resolveGridSpanCount(width)
(rv.layoutManager as? GridLayoutManager)?.spanCount = spanCount
}
}
} }
private fun resolveGridSpanCount(width: Int): Int { private fun resolveGridSpanCount(width: Int) {
val estimatedCount = (width / cellWidth).roundToInt() val estimatedCount = (width / cellWidth).roundToInt()
return estimatedCount.coerceAtLeast(2) spanCount = estimatedCount.coerceAtLeast(2)
} }
} }

View File

@@ -15,11 +15,13 @@ abstract class MangaListViewModel(
) : BaseViewModel() { ) : BaseViewModel() {
abstract val content: LiveData<List<Any>> abstract val content: LiveData<List<Any>>
val isEmptyState = MutableLiveData(false)
val filter = MutableLiveData<MangaFilterConfig>() val filter = MutableLiveData<MangaFilterConfig>()
val listMode = MutableLiveData<ListMode>() val listMode = MutableLiveData<ListMode>()
val gridScale = settings.observe() val gridScale = settings.observe()
.filter { it == AppSettings.KEY_GRID_SIZE } .filter { it == AppSettings.KEY_GRID_SIZE }
.map { settings.gridSize / 100f } .map { settings.gridSize / 100f }
.onStart { emit(settings.gridSize / 100f) }
.asLiveData(viewModelScope.coroutineContext + Dispatchers.IO) .asLiveData(viewModelScope.coroutineContext + Dispatchers.IO)
protected fun createListModeFlow() = settings.observe() protected fun createListModeFlow() = settings.observe()

View File

@@ -14,16 +14,16 @@ import kotlin.jvm.internal.Intrinsics
class MangaListAdapter( class MangaListAdapter(
coil: ImageLoader, coil: ImageLoader,
clickListener: OnListItemClickListener<Manga> clickListener: OnListItemClickListener<Manga>
) : AsyncListDifferDelegationAdapter<Any>(DiffCallback) { ) : AsyncListDifferDelegationAdapter<Any>(DiffCallback()) {
init { init {
delegatesManager.addDelegate(mangaListItemAD(coil, clickListener)) delegatesManager.addDelegate(ITEM_TYPE_MANGA_LIST, mangaListItemAD(coil, clickListener))
.addDelegate(mangaListDetailedItemAD(coil, clickListener)) .addDelegate(ITEM_TYPE_MANGA_LIST_DETAILED, mangaListDetailedItemAD(coil, clickListener))
.addDelegate(mangaGridItemAD(coil, clickListener)) .addDelegate(ITEM_TYPE_MANGA_GRID, mangaGridItemAD(coil, clickListener))
.addDelegate(indeterminateProgressAD()) .addDelegate(ITEM_TYPE_PROGRESS, indeterminateProgressAD())
} }
private companion object DiffCallback : DiffUtil.ItemCallback<Any>() { private class DiffCallback : DiffUtil.ItemCallback<Any>() {
override fun areItemsTheSame(oldItem: Any, newItem: Any) = when { override fun areItemsTheSame(oldItem: Any, newItem: Any) = when {
oldItem is MangaListModel && newItem is MangaListModel -> { oldItem is MangaListModel && newItem is MangaListModel -> {
@@ -44,6 +44,13 @@ class MangaListAdapter(
override fun areContentsTheSame(oldItem: Any, newItem: Any): Boolean { override fun areContentsTheSame(oldItem: Any, newItem: Any): Boolean {
return Intrinsics.areEqual(oldItem, newItem) return Intrinsics.areEqual(oldItem, newItem)
} }
}
companion object {
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
} }
} }

View File

@@ -55,7 +55,7 @@ class LocalListViewModel(
fun importFile(uri: Uri) { fun importFile(uri: Uri) {
launchLoadingJob { launchLoadingJob {
val contentResolver = context.contentResolver val contentResolver = context.contentResolver
val list = withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
val name = MediaStoreCompat.getName(contentResolver, uri) val name = MediaStoreCompat.getName(contentResolver, uri)
?: throw IOException("Cannot fetch name from uri: $uri") ?: throw IOException("Cannot fetch name from uri: $uri")
if (!LocalMangaRepository.isFileSupported(name)) { if (!LocalMangaRepository.isFileSupported(name)) {
@@ -92,7 +92,9 @@ class LocalListViewModel(
private fun loadList() { private fun loadList() {
launchLoadingJob { launchLoadingJob {
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
mangaList.value = repository.getList(0) val list = repository.getList(0)
mangaList.value = list
isEmptyState.postValue(list.isEmpty())
} }
} }
} }

View File

@@ -4,7 +4,6 @@ import android.os.Bundle
import android.view.View import android.view.View
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import kotlinx.android.synthetic.main.dialog_list_mode.button_ok
import kotlinx.android.synthetic.main.dialog_reader_config.* import kotlinx.android.synthetic.main.dialog_reader_config.*
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.AlertDialogFragment import org.koitharu.kotatsu.base.ui.AlertDialogFragment

View File

@@ -3,8 +3,11 @@ package org.koitharu.kotatsu.remotelist.ui
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
@@ -27,13 +30,16 @@ class RemoteListViewModel(
private val mangaList = MutableStateFlow<List<Manga>>(emptyList()) private val mangaList = MutableStateFlow<List<Manga>>(emptyList())
private val hasNextPage = MutableStateFlow(false) private val hasNextPage = MutableStateFlow(false)
private var appliedFilter: MangaFilter? = null private var appliedFilter: MangaFilter? = null
private var loadingJob: Job? = null
override val content = combine(mangaList, createListModeFlow()) { list, mode -> override val content = combine(mangaList.drop(1), createListModeFlow()) { list, mode ->
when(mode) { when(mode) {
ListMode.LIST -> list.map { it.toListModel() } ListMode.LIST -> list.map { it.toListModel() }
ListMode.DETAILED_LIST -> list.map { it.toListDetailedModel() } ListMode.DETAILED_LIST -> list.map { it.toListDetailedModel() }
ListMode.GRID -> list.map { it.toGridModel() } ListMode.GRID -> list.map { it.toGridModel() }
} }
}.onEach {
isEmptyState.postValue(it.isEmpty())
}.combine(hasNextPage) { list, isHasNextPage -> }.combine(hasNextPage) { list, isHasNextPage ->
if (isHasNextPage && list.isNotEmpty()) list + IndeterminateProgress else list if (isHasNextPage && list.isNotEmpty()) list + IndeterminateProgress else list
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default) }.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
@@ -44,7 +50,10 @@ class RemoteListViewModel(
} }
fun loadList(offset: Int) { fun loadList(offset: Int) {
launchLoadingJob { if (loadingJob?.isActive == true) {
return
}
loadingJob = launchLoadingJob {
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
val list = repository.getList( val list = repository.getList(
offset = offset, offset = offset,

View File

@@ -9,7 +9,6 @@ import android.provider.Settings
import android.text.InputType import android.text.InputType
import android.view.View import android.view.View
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.collection.arrayMapOf
import androidx.preference.* import androidx.preference.*
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -22,7 +21,6 @@ import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.ZoomMode import org.koitharu.kotatsu.core.model.ZoomMode
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.list.ui.ListModeSelectDialog
import org.koitharu.kotatsu.settings.utils.MultiSummaryProvider import org.koitharu.kotatsu.settings.utils.MultiSummaryProvider
import org.koitharu.kotatsu.tracker.work.TrackWorker import org.koitharu.kotatsu.tracker.work.TrackWorker
import org.koitharu.kotatsu.utils.ext.getStorageName import org.koitharu.kotatsu.utils.ext.getStorageName
@@ -38,12 +36,6 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings),
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_main) addPreferencesFromResource(R.xml.pref_main)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
findPreference<Preference>(AppSettings.KEY_LIST_MODE)?.summary =
LIST_MODES[settings.listMode]?.let(::getString)
findPreference<SeekBarPreference>(AppSettings.KEY_GRID_SIZE)?.run { findPreference<SeekBarPreference>(AppSettings.KEY_GRID_SIZE)?.run {
summary = "%d%%".format(value) summary = "%d%%".format(value)
setOnPreferenceChangeListener { preference, newValue -> setOnPreferenceChangeListener { preference, newValue ->
@@ -55,6 +47,18 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings),
MultiSummaryProvider(R.string.gestures_only) MultiSummaryProvider(R.string.gestures_only)
findPreference<MultiSelectListPreference>(AppSettings.KEY_TRACK_SOURCES)?.summaryProvider = findPreference<MultiSelectListPreference>(AppSettings.KEY_TRACK_SOURCES)?.summaryProvider =
MultiSummaryProvider(R.string.dont_check) MultiSummaryProvider(R.string.dont_check)
findPreference<ListPreference>(AppSettings.KEY_ZOOM_MODE)?.run {
entryValues = ZoomMode.values().names()
setDefaultValue(ZoomMode.FIT_CENTER.name)
}
findPreference<ListPreference>(AppSettings.KEY_LIST_MODE)?.run {
entryValues = ListMode.values().names()
setDefaultValue(ListMode.GRID.name)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
findPreference<Preference>(AppSettings.KEY_APP_UPDATE_AUTO)?.run { findPreference<Preference>(AppSettings.KEY_APP_UPDATE_AUTO)?.run {
isVisible = AppUpdateChecker.isUpdateSupported(context) isVisible = AppUpdateChecker.isUpdateSupported(context)
} }
@@ -62,10 +66,6 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings),
summary = settings.getStorageDir(context)?.getStorageName(context) summary = settings.getStorageDir(context)?.getStorageName(context)
?: getString(R.string.not_available) ?: getString(R.string.not_available)
} }
findPreference<ListPreference>(AppSettings.KEY_ZOOM_MODE)?.let {
it.entryValues = ZoomMode.values().names()
it.setDefaultValue(ZoomMode.FIT_CENTER.name)
}
findPreference<SwitchPreference>(AppSettings.KEY_PROTECT_APP)?.isChecked = findPreference<SwitchPreference>(AppSettings.KEY_PROTECT_APP)?.isChecked =
!settings.appPassword.isNullOrEmpty() !settings.appPassword.isNullOrEmpty()
findPreference<Preference>(AppSettings.KEY_APP_VERSION)?.run { findPreference<Preference>(AppSettings.KEY_APP_VERSION)?.run {
@@ -82,8 +82,6 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings),
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
when (key) { when (key) {
AppSettings.KEY_LIST_MODE -> findPreference<Preference>(key)?.summary =
LIST_MODES[settings.listMode]?.let(::getString)
AppSettings.KEY_THEME -> { AppSettings.KEY_THEME -> {
AppCompatDelegate.setDefaultNightMode(settings.theme) AppCompatDelegate.setDefaultNightMode(settings.theme)
} }
@@ -111,10 +109,6 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings),
override fun onPreferenceTreeClick(preference: Preference?): Boolean { override fun onPreferenceTreeClick(preference: Preference?): Boolean {
return when (preference?.key) { return when (preference?.key) {
AppSettings.KEY_LIST_MODE -> {
ListModeSelectDialog.show(childFragmentManager)
true
}
AppSettings.KEY_NOTIFICATIONS_SETTINGS -> { AppSettings.KEY_NOTIFICATIONS_SETTINGS -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS) val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
@@ -224,13 +218,4 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings),
} }
} }
} }
private companion object {
val LIST_MODES = arrayMapOf(
ListMode.DETAILED_LIST to R.string.detailed_list,
ListMode.GRID to R.string.grid,
ListMode.LIST to R.string.list
)
}
} }

View File

@@ -1,7 +1,6 @@
package org.koitharu.kotatsu.settings package org.koitharu.kotatsu.settings
import android.os.Bundle import android.os.Bundle
import android.view.View
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference import androidx.preference.MultiSelectListPreference
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
@@ -15,10 +14,6 @@ class ReaderSettingsFragment : BasePreferenceFragment(R.string.reader_settings)
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_reader) addPreferencesFromResource(R.xml.pref_reader)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
findPreference<MultiSelectListPreference>(AppSettings.KEY_READER_SWITCHERS)?.let { findPreference<MultiSelectListPreference>(AppSettings.KEY_READER_SWITCHERS)?.let {
it.summaryProvider = MultiSummaryProvider(R.string.gestures_only) it.summaryProvider = MultiSummaryProvider(R.string.gestures_only)
} }

View File

@@ -2,14 +2,15 @@
<LinearLayout <LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="16dp"
android:orientation="vertical"> android:orientation="vertical">
<com.google.android.material.button.MaterialButtonToggleGroup <com.google.android.material.button.MaterialButtonToggleGroup
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="16dp"
android:orientation="vertical" android:orientation="vertical"
app:selectionRequired="true" app:selectionRequired="true"
app:singleSelection="true"> app:singleSelection="true">
@@ -40,12 +41,21 @@
</com.google.android.material.button.MaterialButtonToggleGroup> </com.google.android.material.button.MaterialButtonToggleGroup>
<com.google.android.material.button.MaterialButton <TextView
android:id="@+id/button_ok" style="?android:attr/windowTitleStyle"
style="@style/Widget.MaterialComponents.Button" android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:paddingLeft="?attr/dialogPreferredPadding"
android:layout_gravity="center_horizontal" android:paddingRight="?attr/dialogPreferredPadding"
android:text="@android:string/ok" /> android:singleLine="true"
android:text="@string/grid_size" />
<SeekBar
android:id="@+id/seekbar_grid"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:max="100"
tools:progress="50" />
</LinearLayout> </LinearLayout>

View File

@@ -19,4 +19,9 @@
<item>@string/favourites</item> <item>@string/favourites</item>
<item>@string/history</item> <item>@string/history</item>
</string-array> </string-array>
<string-array name="list_modes">
<item>@string/list</item>
<item>@string/detailed_list</item>
<item>@string/grid</item>
</string-array>
</resources> </resources>

View File

@@ -8,6 +8,13 @@
<item name="android:paddingBottom">10dp</item> <item name="android:paddingBottom">10dp</item>
</style> </style>
<style name="AppToggleButton.Vertical" parent="Widget.MaterialComponents.Button.OutlinedButton">
<item name="android:checkable">true</item>
<item name="android:gravity">center_horizontal</item>
<item name="iconPadding">6dp</item>
<item name="iconGravity">top</item>
</style>
<style name="AppPopupTheme" parent="ThemeOverlay.MaterialComponents.Light" /> <style name="AppPopupTheme" parent="ThemeOverlay.MaterialComponents.Light" />
<style name="AppToolbarTheme" parent="ThemeOverlay.MaterialComponents.Dark.ActionBar"> <style name="AppToolbarTheme" parent="ThemeOverlay.MaterialComponents.Dark.ActionBar">

View File

@@ -20,10 +20,11 @@
android:title="@string/black_dark_theme" android:title="@string/black_dark_theme"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<Preference <ListPreference
android:key="list_mode" android:key="list_mode_2"
android:persistent="false" android:entries="@array/list_modes"
android:title="@string/list_mode" android:title="@string/list_mode"
app:useSimpleSummaryProvider="true"
app:allowDividerAbove="true" app:allowDividerAbove="true"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />