Fully manga list fragments to AdapterDelegates and mvvm
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
package org.koitharu.kotatsu.base.ui.list
|
||||
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
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 onScrolledToEnd(recyclerView: RecyclerView) {
|
||||
val total = callback.getItemsCount()
|
||||
val total = (recyclerView.layoutManager as? LinearLayoutManager)?.itemCount ?: return
|
||||
if (total > lastTotalCount) {
|
||||
lastTotalCount = total
|
||||
callback.onRequestMoreItems(total)
|
||||
@@ -27,6 +28,7 @@ class PaginationScrollListener(offset: Int, private val callback: Callback) :
|
||||
|
||||
fun onRequestMoreItems(offset: Int)
|
||||
|
||||
fun getItemsCount(): Int
|
||||
@Deprecated("Not in use")
|
||||
fun getItemsCount(): Int = 0
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ class AppSettings private constructor(private val prefs: SharedPreferences) :
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
)
|
||||
|
||||
var listMode by IntEnumPreferenceDelegate(
|
||||
var listMode by EnumPreferenceDelegate(
|
||||
ListMode::class.java,
|
||||
KEY_LIST_MODE,
|
||||
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 gridSize by IntPreferenceDelegate(KEY_GRID_SIZE, defaultValue = 100)
|
||||
var gridSize by IntPreferenceDelegate(KEY_GRID_SIZE, defaultValue = 100)
|
||||
|
||||
val readerPageSwitch by StringSetPreferenceDelegate(
|
||||
KEY_READER_SWITCHERS,
|
||||
@@ -138,7 +138,7 @@ class AppSettings private constructor(private val prefs: SharedPreferences) :
|
||||
const val TRACK_HISTORY = "history"
|
||||
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_THEME = "theme"
|
||||
const val KEY_THEME_AMOLED = "amoled_theme"
|
||||
|
||||
@@ -4,6 +4,8 @@ import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
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.prefs.AppSettings
|
||||
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.toListDetailedModel
|
||||
import org.koitharu.kotatsu.list.ui.model.toListModel
|
||||
import org.koitharu.kotatsu.utils.ext.onFirst
|
||||
|
||||
class FavouritesListViewModel(
|
||||
private val categoryId: Long,
|
||||
@@ -28,6 +31,12 @@ class FavouritesListViewModel(
|
||||
ListMode.DETAILED_LIST -> list.map { it.toListDetailedModel() }
|
||||
ListMode.GRID -> list.map { it.toGridModel() }
|
||||
}
|
||||
}.onEach {
|
||||
isEmptyState.postValue(it.isEmpty())
|
||||
}.onStart {
|
||||
isLoading.postValue(true)
|
||||
}.onFirst {
|
||||
isLoading.postValue(false)
|
||||
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
|
||||
fun removeFromFavourites(manga: Manga) {
|
||||
|
||||
@@ -6,6 +6,8 @@ import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
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.prefs.AppSettings
|
||||
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.utils.MangaShortcut
|
||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||
import org.koitharu.kotatsu.utils.ext.onFirst
|
||||
|
||||
class HistoryListViewModel(
|
||||
private val repository: HistoryRepository,
|
||||
@@ -34,6 +37,12 @@ class HistoryListViewModel(
|
||||
ListMode.DETAILED_LIST -> list.map { it.toListDetailedModel() }
|
||||
ListMode.GRID -> list.map { it.toGridModel() }
|
||||
}
|
||||
}.onEach {
|
||||
isEmptyState.postValue(it.isEmpty())
|
||||
}.onStart {
|
||||
isLoading.postValue(true)
|
||||
}.onFirst {
|
||||
isLoading.postValue(false)
|
||||
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
|
||||
fun clearHistory() {
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.list.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.SeekBar
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.FragmentManager
|
||||
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.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 lateinit var mode: ListMode
|
||||
private var mode: ListMode = ListMode.GRID
|
||||
private var pendingGridSize: Int = 100
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
mode = settings.listMode
|
||||
pendingGridSize = settings.gridSize
|
||||
}
|
||||
|
||||
override fun onBuildDialog(builder: AlertDialog.Builder) {
|
||||
builder.setTitle(R.string.list_mode)
|
||||
.setPositiveButton(R.string.done, null)
|
||||
.setCancelable(true)
|
||||
}
|
||||
|
||||
@@ -33,22 +38,33 @@ class ListModeSelectDialog : AlertDialogFragment(R.layout.dialog_list_mode), Vie
|
||||
button_list_detailed.isChecked = mode == ListMode.DETAILED_LIST
|
||||
button_grid.isChecked = mode == ListMode.GRID
|
||||
|
||||
button_ok.setOnClickListener(this)
|
||||
with(seekbar_grid) {
|
||||
progress = pendingGridSize - 50
|
||||
setOnSeekBarChangeListener(this@ListModeSelectDialog)
|
||||
}
|
||||
|
||||
button_list.setOnClickListener(this)
|
||||
button_grid.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) {
|
||||
when (v.id) {
|
||||
R.id.button_ok -> {
|
||||
settings.listMode = mode
|
||||
dismiss()
|
||||
}
|
||||
R.id.button_list -> mode = ListMode.LIST
|
||||
R.id.button_list_detailed -> mode = ListMode.DETAILED_LIST
|
||||
R.id.button_grid -> mode = ListMode.GRID
|
||||
}
|
||||
settings.listMode = mode
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -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.filter.FilterAdapter
|
||||
import org.koitharu.kotatsu.list.ui.filter.OnFilterChangedListener
|
||||
import org.koitharu.kotatsu.utils.UiUtils
|
||||
import org.koitharu.kotatsu.utils.ext.*
|
||||
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(R.layout.fragment_list),
|
||||
PaginationScrollListener.Callback, OnListItemClickListener<Manga>, OnFilterChangedListener,
|
||||
@@ -41,7 +43,8 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list),
|
||||
|
||||
private var adapter: MangaListAdapter? = null
|
||||
private var paginationListener: PaginationScrollListener? = null
|
||||
private val spanResolver: MangaListSpanResolver? = null
|
||||
private val spanResolver = MangaListSpanResolver()
|
||||
private val spanSizeLookup = SpanSizeLookup()
|
||||
protected var isSwipeRefreshEnabled = true
|
||||
|
||||
protected abstract val viewModel: MangaListViewModel
|
||||
@@ -70,11 +73,13 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list),
|
||||
viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged)
|
||||
viewModel.listMode.observe(viewLifecycleOwner, ::onListModeChanged)
|
||||
viewModel.gridScale.observe(viewLifecycleOwner, ::onGridScaleChanged)
|
||||
viewModel.isEmptyState.observe(viewLifecycleOwner, ::onEmptyStateChanged)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
adapter = null
|
||||
paginationListener = null
|
||||
spanSizeLookup.invalidateCache()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
@@ -126,24 +131,15 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list),
|
||||
}
|
||||
|
||||
private fun onListChanged(list: List<Any>) {
|
||||
spanSizeLookup.invalidateCache()
|
||||
adapter?.items = list
|
||||
if (list.isEmpty()) {
|
||||
setUpEmptyListHolder()
|
||||
layout_holder.isVisible = true
|
||||
} else {
|
||||
layout_holder.isVisible = false
|
||||
}
|
||||
recyclerView.callOnScrollListeners()
|
||||
}
|
||||
|
||||
private fun onError(e: Throwable) {
|
||||
if (e is CloudFlareProtectedException) {
|
||||
CloudFlareDialog.newInstance(e.url).show(childFragmentManager, CloudFlareDialog.TAG)
|
||||
}
|
||||
if (recyclerView.hasItems) {
|
||||
Snackbar.make(recyclerView, e.getDisplayMessage(resources), Snackbar.LENGTH_SHORT)
|
||||
.show()
|
||||
} else {
|
||||
if (viewModel.isEmptyState.value == true) {
|
||||
textView_holder.text = e.getDisplayMessage(resources)
|
||||
textView_holder.setCompoundDrawablesRelativeWithIntrinsicBounds(
|
||||
0,
|
||||
@@ -152,21 +148,29 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list),
|
||||
0
|
||||
)
|
||||
layout_holder.isVisible = true
|
||||
} else {
|
||||
Snackbar.make(recyclerView, e.getDisplayMessage(resources), Snackbar.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
protected open fun onLoadingStateChanged(isLoading: Boolean) {
|
||||
val hasItems = recyclerView.hasItems
|
||||
progressBar.isVisible = isLoading && !hasItems
|
||||
progressBar.isVisible = isLoading && !hasItems && viewModel.isEmptyState.value != true
|
||||
swipeRefreshLayout.isEnabled = isSwipeRefreshEnabled && !progressBar.isVisible
|
||||
if (isLoading) {
|
||||
layout_holder.isVisible = false
|
||||
} else {
|
||||
if (!isLoading) {
|
||||
swipeRefreshLayout.isRefreshing = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun onEmptyStateChanged(isEmpty: Boolean) {
|
||||
if (isEmpty) {
|
||||
setUpEmptyListHolder()
|
||||
}
|
||||
layout_holder.isVisible = isEmpty
|
||||
}
|
||||
|
||||
protected fun onInitFilter(config: MangaFilterConfig) {
|
||||
recyclerView_filter.adapter = FilterAdapter(
|
||||
sortOrders = config.sortOrders,
|
||||
@@ -198,12 +202,15 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list),
|
||||
}
|
||||
|
||||
private fun onGridScaleChanged(scale: Float) {
|
||||
UiUtils.SpanCountResolver.update(recyclerView)
|
||||
spanSizeLookup.invalidateCache()
|
||||
spanResolver.setGridSize(scale, recyclerView)
|
||||
}
|
||||
|
||||
private fun onListModeChanged(mode: ListMode) {
|
||||
spanSizeLookup.invalidateCache()
|
||||
with(recyclerView) {
|
||||
clearItemDecorations()
|
||||
removeOnLayoutChangeListener(spanResolver)
|
||||
when (mode) {
|
||||
ListMode.LIST -> {
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
@@ -216,52 +223,27 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list),
|
||||
}
|
||||
ListMode.DETAILED_LIST -> {
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
}
|
||||
ListMode.GRID -> {
|
||||
layoutManager = GridLayoutManager(context, 3)
|
||||
addItemDecoration(
|
||||
SpacingItemDecoration(
|
||||
resources.getDimensionPixelOffset(R.dimen.grid_spacing)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
ListMode.GRID -> {
|
||||
layoutManager = GridLayoutManager(context, spanResolver.spanCount).also {
|
||||
it.spanSizeLookup = spanSizeLookup
|
||||
}
|
||||
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 {
|
||||
return position == 0 || recyclerView_filter.adapter?.run {
|
||||
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 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,20 @@
|
||||
package org.koitharu.kotatsu.list.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class MangaListSpanResolver(
|
||||
context: Context,
|
||||
private val adapter: MangaListAdapter
|
||||
) : GridLayoutManager.SpanSizeLookup(), View.OnLayoutChangeListener {
|
||||
class MangaListSpanResolver : 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
|
||||
|
||||
override fun getSpanSize(position: Int) = when(adapter.getItemViewType(position)) {
|
||||
else -> 1
|
||||
}
|
||||
|
||||
override fun onLayoutChange(
|
||||
v: View?,
|
||||
left: Int,
|
||||
@@ -36,20 +30,33 @@ class MangaListSpanResolver(
|
||||
return
|
||||
}
|
||||
val rv = v as? RecyclerView ?: return
|
||||
if (gridWidth < 0f) {
|
||||
gridWidth = rv.resources.getDimension(R.dimen.preferred_grid_width)
|
||||
}
|
||||
val width = abs(right - left)
|
||||
if (width == 0) {
|
||||
return
|
||||
}
|
||||
(rv.layoutManager as? GridLayoutManager)?.spanCount = resolveGridSpanCount(width)
|
||||
resolveGridSpanCount(width)
|
||||
(rv.layoutManager as? GridLayoutManager)?.spanCount = spanCount
|
||||
}
|
||||
|
||||
fun setGridSize(gridSize: Int) {
|
||||
val scaleFactor = gridSize / 100f
|
||||
fun setGridSize(scaleFactor: Float, rv: RecyclerView?) {
|
||||
if (gridWidth < 0f) {
|
||||
gridWidth = (rv ?: return).resources.getDimension(R.dimen.preferred_grid_width)
|
||||
}
|
||||
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()
|
||||
return estimatedCount.coerceAtLeast(2)
|
||||
spanCount = estimatedCount.coerceAtLeast(2)
|
||||
}
|
||||
}
|
||||
@@ -15,11 +15,13 @@ abstract class MangaListViewModel(
|
||||
) : BaseViewModel() {
|
||||
|
||||
abstract val content: LiveData<List<Any>>
|
||||
val isEmptyState = MutableLiveData(false)
|
||||
val filter = MutableLiveData<MangaFilterConfig>()
|
||||
val listMode = MutableLiveData<ListMode>()
|
||||
val gridScale = settings.observe()
|
||||
.filter { it == AppSettings.KEY_GRID_SIZE }
|
||||
.map { settings.gridSize / 100f }
|
||||
.onStart { emit(settings.gridSize / 100f) }
|
||||
.asLiveData(viewModelScope.coroutineContext + Dispatchers.IO)
|
||||
|
||||
protected fun createListModeFlow() = settings.observe()
|
||||
|
||||
@@ -14,16 +14,16 @@ import kotlin.jvm.internal.Intrinsics
|
||||
class MangaListAdapter(
|
||||
coil: ImageLoader,
|
||||
clickListener: OnListItemClickListener<Manga>
|
||||
) : AsyncListDifferDelegationAdapter<Any>(DiffCallback) {
|
||||
) : AsyncListDifferDelegationAdapter<Any>(DiffCallback()) {
|
||||
|
||||
init {
|
||||
delegatesManager.addDelegate(mangaListItemAD(coil, clickListener))
|
||||
.addDelegate(mangaListDetailedItemAD(coil, clickListener))
|
||||
.addDelegate(mangaGridItemAD(coil, clickListener))
|
||||
.addDelegate(indeterminateProgressAD())
|
||||
delegatesManager.addDelegate(ITEM_TYPE_MANGA_LIST, mangaListItemAD(coil, clickListener))
|
||||
.addDelegate(ITEM_TYPE_MANGA_LIST_DETAILED, mangaListDetailedItemAD(coil, clickListener))
|
||||
.addDelegate(ITEM_TYPE_MANGA_GRID, mangaGridItemAD(coil, clickListener))
|
||||
.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 {
|
||||
oldItem is MangaListModel && newItem is MangaListModel -> {
|
||||
@@ -44,6 +44,13 @@ class MangaListAdapter(
|
||||
override fun areContentsTheSame(oldItem: Any, newItem: Any): Boolean {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,7 @@ class LocalListViewModel(
|
||||
fun importFile(uri: Uri) {
|
||||
launchLoadingJob {
|
||||
val contentResolver = context.contentResolver
|
||||
val list = withContext(Dispatchers.Default) {
|
||||
withContext(Dispatchers.Default) {
|
||||
val name = MediaStoreCompat.getName(contentResolver, uri)
|
||||
?: throw IOException("Cannot fetch name from uri: $uri")
|
||||
if (!LocalMangaRepository.isFileSupported(name)) {
|
||||
@@ -92,7 +92,9 @@ class LocalListViewModel(
|
||||
private fun loadList() {
|
||||
launchLoadingJob {
|
||||
withContext(Dispatchers.Default) {
|
||||
mangaList.value = repository.getList(0)
|
||||
val list = repository.getList(0)
|
||||
mangaList.value = list
|
||||
isEmptyState.postValue(list.isEmpty())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import kotlinx.android.synthetic.main.dialog_list_mode.button_ok
|
||||
import kotlinx.android.synthetic.main.dialog_reader_config.*
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
|
||||
|
||||
@@ -3,8 +3,11 @@ package org.koitharu.kotatsu.remotelist.ui
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
@@ -27,13 +30,16 @@ class RemoteListViewModel(
|
||||
private val mangaList = MutableStateFlow<List<Manga>>(emptyList())
|
||||
private val hasNextPage = MutableStateFlow(false)
|
||||
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) {
|
||||
ListMode.LIST -> list.map { it.toListModel() }
|
||||
ListMode.DETAILED_LIST -> list.map { it.toListDetailedModel() }
|
||||
ListMode.GRID -> list.map { it.toGridModel() }
|
||||
}
|
||||
}.onEach {
|
||||
isEmptyState.postValue(it.isEmpty())
|
||||
}.combine(hasNextPage) { list, isHasNextPage ->
|
||||
if (isHasNextPage && list.isNotEmpty()) list + IndeterminateProgress else list
|
||||
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
@@ -44,7 +50,10 @@ class RemoteListViewModel(
|
||||
}
|
||||
|
||||
fun loadList(offset: Int) {
|
||||
launchLoadingJob {
|
||||
if (loadingJob?.isActive == true) {
|
||||
return
|
||||
}
|
||||
loadingJob = launchLoadingJob {
|
||||
withContext(Dispatchers.Default) {
|
||||
val list = repository.getList(
|
||||
offset = offset,
|
||||
|
||||
@@ -9,7 +9,6 @@ import android.provider.Settings
|
||||
import android.text.InputType
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.collection.arrayMapOf
|
||||
import androidx.preference.*
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
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.prefs.AppSettings
|
||||
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.tracker.work.TrackWorker
|
||||
import org.koitharu.kotatsu.utils.ext.getStorageName
|
||||
@@ -38,12 +36,6 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings),
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
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 {
|
||||
summary = "%d%%".format(value)
|
||||
setOnPreferenceChangeListener { preference, newValue ->
|
||||
@@ -55,6 +47,18 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings),
|
||||
MultiSummaryProvider(R.string.gestures_only)
|
||||
findPreference<MultiSelectListPreference>(AppSettings.KEY_TRACK_SOURCES)?.summaryProvider =
|
||||
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 {
|
||||
isVisible = AppUpdateChecker.isUpdateSupported(context)
|
||||
}
|
||||
@@ -62,10 +66,6 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings),
|
||||
summary = settings.getStorageDir(context)?.getStorageName(context)
|
||||
?: 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 =
|
||||
!settings.appPassword.isNullOrEmpty()
|
||||
findPreference<Preference>(AppSettings.KEY_APP_VERSION)?.run {
|
||||
@@ -82,8 +82,6 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings),
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
|
||||
when (key) {
|
||||
AppSettings.KEY_LIST_MODE -> findPreference<Preference>(key)?.summary =
|
||||
LIST_MODES[settings.listMode]?.let(::getString)
|
||||
AppSettings.KEY_THEME -> {
|
||||
AppCompatDelegate.setDefaultNightMode(settings.theme)
|
||||
}
|
||||
@@ -111,10 +109,6 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings),
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference?): Boolean {
|
||||
return when (preference?.key) {
|
||||
AppSettings.KEY_LIST_MODE -> {
|
||||
ListModeSelectDialog.show(childFragmentManager)
|
||||
true
|
||||
}
|
||||
AppSettings.KEY_NOTIFICATIONS_SETTINGS -> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.koitharu.kotatsu.settings
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.MultiSelectListPreference
|
||||
import org.koitharu.kotatsu.R
|
||||
@@ -15,10 +14,6 @@ class ReaderSettingsFragment : BasePreferenceFragment(R.string.reader_settings)
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.pref_reader)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
findPreference<MultiSelectListPreference>(AppSettings.KEY_READER_SWITCHERS)?.let {
|
||||
it.summaryProvider = MultiSummaryProvider(R.string.gestures_only)
|
||||
}
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
<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="wrap_content"
|
||||
android:padding="16dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.button.MaterialButtonToggleGroup
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:orientation="vertical"
|
||||
app:selectionRequired="true"
|
||||
app:singleSelection="true">
|
||||
@@ -40,12 +41,21 @@
|
||||
|
||||
</com.google.android.material.button.MaterialButtonToggleGroup>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_ok"
|
||||
style="@style/Widget.MaterialComponents.Button"
|
||||
android:layout_width="wrap_content"
|
||||
<TextView
|
||||
style="?android:attr/windowTitleStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:text="@android:string/ok" />
|
||||
android:paddingLeft="?attr/dialogPreferredPadding"
|
||||
android:paddingRight="?attr/dialogPreferredPadding"
|
||||
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>
|
||||
@@ -19,4 +19,9 @@
|
||||
<item>@string/favourites</item>
|
||||
<item>@string/history</item>
|
||||
</string-array>
|
||||
<string-array name="list_modes">
|
||||
<item>@string/list</item>
|
||||
<item>@string/detailed_list</item>
|
||||
<item>@string/grid</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
@@ -8,6 +8,13 @@
|
||||
<item name="android:paddingBottom">10dp</item>
|
||||
</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="AppToolbarTheme" parent="ThemeOverlay.MaterialComponents.Dark.ActionBar">
|
||||
|
||||
@@ -20,10 +20,11 @@
|
||||
android:title="@string/black_dark_theme"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<Preference
|
||||
android:key="list_mode"
|
||||
android:persistent="false"
|
||||
<ListPreference
|
||||
android:key="list_mode_2"
|
||||
android:entries="@array/list_modes"
|
||||
android:title="@string/list_mode"
|
||||
app:useSimpleSummaryProvider="true"
|
||||
app:allowDividerAbove="true"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
|
||||
Reference in New Issue
Block a user