Merge branch 'devel' into feature/mal
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -12,6 +12,7 @@
|
|||||||
/.idea/navEditor.xml
|
/.idea/navEditor.xml
|
||||||
/.idea/assetWizardSettings.xml
|
/.idea/assetWizardSettings.xml
|
||||||
/.idea/kotlinScripting.xml
|
/.idea/kotlinScripting.xml
|
||||||
|
/.idea/kotlinc.xml
|
||||||
/.idea/deploymentTargetDropDown.xml
|
/.idea/deploymentTargetDropDown.xml
|
||||||
/.idea/androidTestResultsUserPreferences.xml
|
/.idea/androidTestResultsUserPreferences.xml
|
||||||
/.idea/render.experimental.xml
|
/.idea/render.experimental.xml
|
||||||
|
|||||||
9
.idea/kotlinc.xml
generated
9
.idea/kotlinc.xml
generated
@@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="Kotlin2JvmCompilerArguments">
|
|
||||||
<option name="jvmTarget" value="1.8" />
|
|
||||||
</component>
|
|
||||||
<component name="KotlinJpsPluginSettings">
|
|
||||||
<option name="version" value="1.8.0" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
@@ -90,7 +90,7 @@ dependencies {
|
|||||||
exclude group: 'org.json', module: 'json'
|
exclude group: 'org.json', module: 'json'
|
||||||
}
|
}
|
||||||
|
|
||||||
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.8.0'
|
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.8.10'
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
|
||||||
|
|
||||||
implementation "androidx.appcompat:appcompat:1.6.0"
|
implementation "androidx.appcompat:appcompat:1.6.0"
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package org.koitharu.kotatsu.base.ui.util
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import androidx.annotation.Px
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import org.koitharu.kotatsu.parsers.util.toIntUp
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
class SpanSizeResolver(
|
||||||
|
private val recyclerView: RecyclerView,
|
||||||
|
@Px private val minItemWidth: Int,
|
||||||
|
) : View.OnLayoutChangeListener {
|
||||||
|
|
||||||
|
fun attach() {
|
||||||
|
recyclerView.addOnLayoutChangeListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun detach() {
|
||||||
|
recyclerView.removeOnLayoutChangeListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLayoutChange(
|
||||||
|
v: View?,
|
||||||
|
left: Int,
|
||||||
|
top: Int,
|
||||||
|
right: Int,
|
||||||
|
bottom: Int,
|
||||||
|
oldLeft: Int,
|
||||||
|
oldTop: Int,
|
||||||
|
oldRight: Int,
|
||||||
|
oldBottom: Int,
|
||||||
|
) {
|
||||||
|
invalidateInternal(abs(right - left))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun invalidate() {
|
||||||
|
invalidateInternal(recyclerView.width)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun invalidateInternal(width: Int) {
|
||||||
|
if (width <= 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val lm = recyclerView.layoutManager as? GridLayoutManager ?: return
|
||||||
|
val estimatedCount = (width / minItemWidth.toFloat()).toIntUp()
|
||||||
|
if (lm.spanCount != estimatedCount) {
|
||||||
|
lm.spanCount = estimatedCount
|
||||||
|
lm.spanSizeLookup?.run {
|
||||||
|
invalidateSpanGroupIndexCache()
|
||||||
|
invalidateSpanIndexCache()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -205,6 +205,10 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
sourcesOrder = (sourcesOrder + sources.map { it.name }).distinct()
|
sourcesOrder = (sourcesOrder + sources.map { it.name }).distinct()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isSourcesGridMode: Boolean
|
||||||
|
get() = prefs.getBoolean(KEY_SOURCES_GRID, false)
|
||||||
|
set(value) = prefs.edit { putBoolean(KEY_SOURCES_GRID, value) }
|
||||||
|
|
||||||
val isPagesNumbersEnabled: Boolean
|
val isPagesNumbersEnabled: Boolean
|
||||||
get() = prefs.getBoolean(KEY_PAGES_NUMBERS, false)
|
get() = prefs.getBoolean(KEY_PAGES_NUMBERS, false)
|
||||||
|
|
||||||
@@ -377,6 +381,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
const val KEY_APP_LOCALE = "app_locale"
|
const val KEY_APP_LOCALE = "app_locale"
|
||||||
const val KEY_LOGGING_ENABLED = "logging"
|
const val KEY_LOGGING_ENABLED = "logging"
|
||||||
const val KEY_LOGS_SHARE = "logs_share"
|
const val KEY_LOGS_SHARE = "logs_share"
|
||||||
|
const val KEY_SOURCES_GRID = "sources_grid"
|
||||||
|
|
||||||
// About
|
// About
|
||||||
const val KEY_APP_UPDATE = "app_update"
|
const val KEY_APP_UPDATE = "app_update"
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
package org.koitharu.kotatsu.core.prefs
|
package org.koitharu.kotatsu.core.prefs
|
||||||
|
|
||||||
import androidx.lifecycle.liveData
|
import androidx.lifecycle.liveData
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.flow.transform
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
fun <T> AppSettings.observeAsFlow(key: String, valueProducer: AppSettings.() -> T) = flow {
|
fun <T> AppSettings.observeAsFlow(key: String, valueProducer: AppSettings.() -> T) = flow {
|
||||||
var lastValue: T = valueProducer()
|
var lastValue: T = valueProducer()
|
||||||
@@ -33,3 +38,13 @@ fun <T> AppSettings.observeAsLiveData(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun <T> AppSettings.observeAsStateFlow(
|
||||||
|
key: String,
|
||||||
|
scope: CoroutineScope,
|
||||||
|
valueProducer: AppSettings.() -> T,
|
||||||
|
): StateFlow<T> = observe().transform {
|
||||||
|
if (it == key) {
|
||||||
|
emit(valueProducer())
|
||||||
|
}
|
||||||
|
}.stateIn(scope, SharingStarted.Eagerly, valueProducer())
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import androidx.appcompat.widget.PopupMenu
|
|||||||
import androidx.core.graphics.Insets
|
import androidx.core.graphics.Insets
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
@@ -19,6 +21,7 @@ import org.koitharu.kotatsu.base.ui.BaseFragment
|
|||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||||
import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner
|
import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner
|
||||||
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
|
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
|
||||||
|
import org.koitharu.kotatsu.base.ui.util.SpanSizeResolver
|
||||||
import org.koitharu.kotatsu.bookmarks.ui.BookmarksActivity
|
import org.koitharu.kotatsu.bookmarks.ui.BookmarksActivity
|
||||||
import org.koitharu.kotatsu.databinding.FragmentExploreBinding
|
import org.koitharu.kotatsu.databinding.FragmentExploreBinding
|
||||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||||
@@ -63,6 +66,7 @@ class ExploreFragment :
|
|||||||
with(binding.recyclerView) {
|
with(binding.recyclerView) {
|
||||||
adapter = exploreAdapter
|
adapter = exploreAdapter
|
||||||
setHasFixedSize(true)
|
setHasFixedSize(true)
|
||||||
|
SpanSizeResolver(this, resources.getDimensionPixelSize(R.dimen.explore_grid_width)).attach()
|
||||||
val spacing = resources.getDimensionPixelOffset(R.dimen.list_spacing)
|
val spacing = resources.getDimensionPixelOffset(R.dimen.list_spacing)
|
||||||
paddingHorizontal = spacing
|
paddingHorizontal = spacing
|
||||||
}
|
}
|
||||||
@@ -72,6 +76,7 @@ class ExploreFragment :
|
|||||||
viewModel.onError.observe(viewLifecycleOwner, ::onError)
|
viewModel.onError.observe(viewLifecycleOwner, ::onError)
|
||||||
viewModel.onOpenManga.observe(viewLifecycleOwner, ::onOpenManga)
|
viewModel.onOpenManga.observe(viewLifecycleOwner, ::onOpenManga)
|
||||||
viewModel.onActionDone.observe(viewLifecycleOwner, ::onActionDone)
|
viewModel.onActionDone.observe(viewLifecycleOwner, ::onActionDone)
|
||||||
|
viewModel.isGrid.observe(viewLifecycleOwner, ::onGridModeChanged)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
@@ -149,6 +154,16 @@ class ExploreFragment :
|
|||||||
snackbar.show()
|
snackbar.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onGridModeChanged(isGrid: Boolean) {
|
||||||
|
binding.recyclerView.layoutManager = if (isGrid) {
|
||||||
|
GridLayoutManager(requireContext(), 4).also { lm ->
|
||||||
|
lm.spanSizeLookup = ExploreGridSpanSizeLookup(checkNotNull(exploreAdapter), lm)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LinearLayoutManager(requireContext())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private inner class SourceMenuListener(
|
private inner class SourceMenuListener(
|
||||||
private val sourceItem: ExploreItem.Source,
|
private val sourceItem: ExploreItem.Source,
|
||||||
) : PopupMenu.OnMenuItemClickListener {
|
) : PopupMenu.OnMenuItemClickListener {
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package org.koitharu.kotatsu.explore.ui
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup
|
||||||
|
import org.koitharu.kotatsu.explore.ui.adapter.ExploreAdapter
|
||||||
|
|
||||||
|
class ExploreGridSpanSizeLookup(
|
||||||
|
private val adapter: ExploreAdapter,
|
||||||
|
private val layoutManager: GridLayoutManager,
|
||||||
|
) : SpanSizeLookup() {
|
||||||
|
|
||||||
|
override fun getSpanSize(position: Int): Int {
|
||||||
|
val itemType = adapter.getItemViewType(position)
|
||||||
|
return if (itemType == ExploreAdapter.ITEM_TYPE_SOURCE_GRID) 1 else layoutManager.spanCount
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,22 +5,26 @@ import androidx.lifecycle.asFlow
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.onStart
|
||||||
|
import kotlinx.coroutines.plus
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.domain.ReversibleHandle
|
import org.koitharu.kotatsu.base.domain.ReversibleHandle
|
||||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||||
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
|
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
|
||||||
import org.koitharu.kotatsu.explore.domain.ExploreRepository
|
import org.koitharu.kotatsu.explore.domain.ExploreRepository
|
||||||
import org.koitharu.kotatsu.explore.ui.model.ExploreItem
|
import org.koitharu.kotatsu.explore.ui.model.ExploreItem
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||||
|
import org.koitharu.kotatsu.utils.asFlowLiveData
|
||||||
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -30,8 +34,15 @@ class ExploreViewModel @Inject constructor(
|
|||||||
private val exploreRepository: ExploreRepository,
|
private val exploreRepository: ExploreRepository,
|
||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
|
private val gridMode = settings.observeAsStateFlow(
|
||||||
|
key = AppSettings.KEY_SOURCES_GRID,
|
||||||
|
scope = viewModelScope + Dispatchers.IO,
|
||||||
|
valueProducer = { isSourcesGridMode },
|
||||||
|
)
|
||||||
|
|
||||||
val onOpenManga = SingleLiveEvent<Manga>()
|
val onOpenManga = SingleLiveEvent<Manga>()
|
||||||
val onActionDone = SingleLiveEvent<ReversibleAction>()
|
val onActionDone = SingleLiveEvent<ReversibleAction>()
|
||||||
|
val isGrid = gridMode.asFlowLiveData(viewModelScope.coroutineContext)
|
||||||
|
|
||||||
val content: LiveData<List<ExploreItem>> = isLoading.asFlow().flatMapLatest { loading ->
|
val content: LiveData<List<ExploreItem>> = isLoading.asFlow().flatMapLatest { loading ->
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -67,16 +78,16 @@ class ExploreViewModel @Inject constructor(
|
|||||||
.onStart { emit("") }
|
.onStart { emit("") }
|
||||||
.map { settings.getMangaSources(includeHidden = false) }
|
.map { settings.getMangaSources(includeHidden = false) }
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.map { buildList(it) }
|
.combine(gridMode) { content, grid -> buildList(content, grid) }
|
||||||
|
|
||||||
private fun buildList(sources: List<MangaSource>): List<ExploreItem> {
|
private fun buildList(sources: List<MangaSource>, isGrid: Boolean): List<ExploreItem> {
|
||||||
val result = ArrayList<ExploreItem>(sources.size + 3)
|
val result = ArrayList<ExploreItem>(sources.size + 3)
|
||||||
result += ExploreItem.Buttons(
|
result += ExploreItem.Buttons(
|
||||||
isSuggestionsEnabled = settings.isSuggestionsEnabled,
|
isSuggestionsEnabled = settings.isSuggestionsEnabled,
|
||||||
)
|
)
|
||||||
result += ExploreItem.Header(R.string.remote_sources, sources.isNotEmpty())
|
result += ExploreItem.Header(R.string.remote_sources, sources.isNotEmpty())
|
||||||
if (sources.isNotEmpty()) {
|
if (sources.isNotEmpty()) {
|
||||||
sources.mapTo(result) { ExploreItem.Source(it) }
|
sources.mapTo(result) { ExploreItem.Source(it, isGrid) }
|
||||||
} else {
|
} else {
|
||||||
result += ExploreItem.EmptyHint(
|
result += ExploreItem.EmptyHint(
|
||||||
icon = R.drawable.ic_empty_common,
|
icon = R.drawable.ic_empty_common,
|
||||||
|
|||||||
@@ -11,11 +11,25 @@ class ExploreAdapter(
|
|||||||
lifecycleOwner: LifecycleOwner,
|
lifecycleOwner: LifecycleOwner,
|
||||||
listener: ExploreListEventListener,
|
listener: ExploreListEventListener,
|
||||||
clickListener: OnListItemClickListener<ExploreItem.Source>,
|
clickListener: OnListItemClickListener<ExploreItem.Source>,
|
||||||
) : AsyncListDifferDelegationAdapter<ExploreItem>(
|
) : AsyncListDifferDelegationAdapter<ExploreItem>(ExploreDiffCallback()) {
|
||||||
ExploreDiffCallback(),
|
|
||||||
exploreButtonsAD(listener),
|
init {
|
||||||
exploreSourcesHeaderAD(listener),
|
delegatesManager
|
||||||
exploreSourceItemAD(coil, clickListener, lifecycleOwner),
|
.addDelegate(ITEM_TYPE_BUTTONS, exploreButtonsAD(listener))
|
||||||
exploreEmptyHintListAD(listener),
|
.addDelegate(ITEM_TYPE_HEADER, exploreSourcesHeaderAD(listener))
|
||||||
exploreLoadingAD(),
|
.addDelegate(ITEM_TYPE_SOURCE_LIST, exploreSourceListItemAD(coil, clickListener, lifecycleOwner))
|
||||||
)
|
.addDelegate(ITEM_TYPE_SOURCE_GRID, exploreSourceGridItemAD(coil, clickListener, lifecycleOwner))
|
||||||
|
.addDelegate(ITEM_TYPE_HINT, exploreEmptyHintListAD(listener))
|
||||||
|
.addDelegate(ITEM_TYPE_LOADING, exploreLoadingAD())
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val ITEM_TYPE_BUTTONS = 0
|
||||||
|
const val ITEM_TYPE_HEADER = 1
|
||||||
|
const val ITEM_TYPE_SOURCE_LIST = 2
|
||||||
|
const val ITEM_TYPE_SOURCE_GRID = 3
|
||||||
|
const val ITEM_TYPE_HINT = 4
|
||||||
|
const val ITEM_TYPE_LOADING = 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
|||||||
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
|
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
|
||||||
import org.koitharu.kotatsu.databinding.ItemEmptyCardBinding
|
import org.koitharu.kotatsu.databinding.ItemEmptyCardBinding
|
||||||
import org.koitharu.kotatsu.databinding.ItemExploreButtonsBinding
|
import org.koitharu.kotatsu.databinding.ItemExploreButtonsBinding
|
||||||
import org.koitharu.kotatsu.databinding.ItemExploreSourceBinding
|
import org.koitharu.kotatsu.databinding.ItemExploreSourceGridBinding
|
||||||
|
import org.koitharu.kotatsu.databinding.ItemExploreSourceListBinding
|
||||||
import org.koitharu.kotatsu.databinding.ItemHeaderButtonBinding
|
import org.koitharu.kotatsu.databinding.ItemHeaderButtonBinding
|
||||||
import org.koitharu.kotatsu.explore.ui.model.ExploreItem
|
import org.koitharu.kotatsu.explore.ui.model.ExploreItem
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
|
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
|
||||||
@@ -25,7 +26,7 @@ import org.koitharu.kotatsu.utils.image.FaviconFallbackDrawable
|
|||||||
fun exploreButtonsAD(
|
fun exploreButtonsAD(
|
||||||
clickListener: View.OnClickListener,
|
clickListener: View.OnClickListener,
|
||||||
) = adapterDelegateViewBinding<ExploreItem.Buttons, ExploreItem, ItemExploreButtonsBinding>(
|
) = adapterDelegateViewBinding<ExploreItem.Buttons, ExploreItem, ItemExploreButtonsBinding>(
|
||||||
{ layoutInflater, parent -> ItemExploreButtonsBinding.inflate(layoutInflater, parent, false) }
|
{ layoutInflater, parent -> ItemExploreButtonsBinding.inflate(layoutInflater, parent, false) },
|
||||||
) {
|
) {
|
||||||
|
|
||||||
binding.buttonBookmarks.setOnClickListener(clickListener)
|
binding.buttonBookmarks.setOnClickListener(clickListener)
|
||||||
@@ -43,7 +44,7 @@ fun exploreButtonsAD(
|
|||||||
fun exploreSourcesHeaderAD(
|
fun exploreSourcesHeaderAD(
|
||||||
listener: ExploreListEventListener,
|
listener: ExploreListEventListener,
|
||||||
) = adapterDelegateViewBinding<ExploreItem.Header, ExploreItem, ItemHeaderButtonBinding>(
|
) = adapterDelegateViewBinding<ExploreItem.Header, ExploreItem, ItemHeaderButtonBinding>(
|
||||||
{ layoutInflater, parent -> ItemHeaderButtonBinding.inflate(layoutInflater, parent, false) }
|
{ layoutInflater, parent -> ItemHeaderButtonBinding.inflate(layoutInflater, parent, false) },
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val listenerAdapter = View.OnClickListener {
|
val listenerAdapter = View.OnClickListener {
|
||||||
@@ -58,13 +59,44 @@ fun exploreSourcesHeaderAD(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun exploreSourceItemAD(
|
fun exploreSourceListItemAD(
|
||||||
coil: ImageLoader,
|
coil: ImageLoader,
|
||||||
listener: OnListItemClickListener<ExploreItem.Source>,
|
listener: OnListItemClickListener<ExploreItem.Source>,
|
||||||
lifecycleOwner: LifecycleOwner,
|
lifecycleOwner: LifecycleOwner,
|
||||||
) = adapterDelegateViewBinding<ExploreItem.Source, ExploreItem, ItemExploreSourceBinding>(
|
) = adapterDelegateViewBinding<ExploreItem.Source, ExploreItem, ItemExploreSourceListBinding>(
|
||||||
{ layoutInflater, parent -> ItemExploreSourceBinding.inflate(layoutInflater, parent, false) },
|
{ layoutInflater, parent -> ItemExploreSourceListBinding.inflate(layoutInflater, parent, false) },
|
||||||
on = { item, _, _ -> item is ExploreItem.Source }
|
on = { item, _, _ -> item is ExploreItem.Source && !item.isGrid },
|
||||||
|
) {
|
||||||
|
|
||||||
|
val eventListener = AdapterDelegateClickListenerAdapter(this, listener)
|
||||||
|
|
||||||
|
binding.root.setOnClickListener(eventListener)
|
||||||
|
binding.root.setOnLongClickListener(eventListener)
|
||||||
|
|
||||||
|
bind {
|
||||||
|
binding.textViewTitle.text = item.source.title
|
||||||
|
val fallbackIcon = FaviconFallbackDrawable(context, item.source.name)
|
||||||
|
binding.imageViewIcon.newImageRequest(item.source.faviconUri())?.run {
|
||||||
|
fallback(fallbackIcon)
|
||||||
|
placeholder(fallbackIcon)
|
||||||
|
error(fallbackIcon)
|
||||||
|
lifecycle(lifecycleOwner)
|
||||||
|
enqueueWith(coil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onViewRecycled {
|
||||||
|
binding.imageViewIcon.disposeImageRequest()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun exploreSourceGridItemAD(
|
||||||
|
coil: ImageLoader,
|
||||||
|
listener: OnListItemClickListener<ExploreItem.Source>,
|
||||||
|
lifecycleOwner: LifecycleOwner,
|
||||||
|
) = adapterDelegateViewBinding<ExploreItem.Source, ExploreItem, ItemExploreSourceGridBinding>(
|
||||||
|
{ layoutInflater, parent -> ItemExploreSourceGridBinding.inflate(layoutInflater, parent, false) },
|
||||||
|
on = { item, _, _ -> item is ExploreItem.Source && item.isGrid },
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val eventListener = AdapterDelegateClickListenerAdapter(this, listener)
|
val eventListener = AdapterDelegateClickListenerAdapter(this, listener)
|
||||||
@@ -92,7 +124,7 @@ fun exploreSourceItemAD(
|
|||||||
fun exploreEmptyHintListAD(
|
fun exploreEmptyHintListAD(
|
||||||
listener: ListStateHolderListener,
|
listener: ListStateHolderListener,
|
||||||
) = adapterDelegateViewBinding<ExploreItem.EmptyHint, ExploreItem, ItemEmptyCardBinding>(
|
) = adapterDelegateViewBinding<ExploreItem.EmptyHint, ExploreItem, ItemEmptyCardBinding>(
|
||||||
{ inflater, parent -> ItemEmptyCardBinding.inflate(inflater, parent, false) }
|
{ inflater, parent -> ItemEmptyCardBinding.inflate(inflater, parent, false) },
|
||||||
) {
|
) {
|
||||||
|
|
||||||
binding.buttonRetry.setOnClickListener { listener.onEmptyActionClick() }
|
binding.buttonRetry.setOnClickListener { listener.onEmptyActionClick() }
|
||||||
|
|||||||
@@ -12,11 +12,13 @@ class ExploreDiffCallback : DiffUtil.ItemCallback<ExploreItem>() {
|
|||||||
oldItem is ExploreItem.Loading && newItem is ExploreItem.Loading -> true
|
oldItem is ExploreItem.Loading && newItem is ExploreItem.Loading -> true
|
||||||
oldItem is ExploreItem.EmptyHint && newItem is ExploreItem.EmptyHint -> true
|
oldItem is ExploreItem.EmptyHint && newItem is ExploreItem.EmptyHint -> true
|
||||||
oldItem is ExploreItem.Source && newItem is ExploreItem.Source -> {
|
oldItem is ExploreItem.Source && newItem is ExploreItem.Source -> {
|
||||||
oldItem.source == newItem.source
|
oldItem.source == newItem.source && oldItem.isGrid == newItem.isGrid
|
||||||
}
|
}
|
||||||
|
|
||||||
oldItem is ExploreItem.Header && newItem is ExploreItem.Header -> {
|
oldItem is ExploreItem.Header && newItem is ExploreItem.Header -> {
|
||||||
oldItem.titleResId == newItem.titleResId
|
oldItem.titleResId == newItem.titleResId
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -24,4 +26,4 @@ class ExploreDiffCallback : DiffUtil.ItemCallback<ExploreItem>() {
|
|||||||
override fun areContentsTheSame(oldItem: ExploreItem, newItem: ExploreItem): Boolean {
|
override fun areContentsTheSame(oldItem: ExploreItem, newItem: ExploreItem): Boolean {
|
||||||
return oldItem == newItem
|
return oldItem == newItem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ sealed interface ExploreItem : ListModel {
|
|||||||
|
|
||||||
class Source(
|
class Source(
|
||||||
val source: MangaSource,
|
val source: MangaSource,
|
||||||
|
val isGrid: Boolean,
|
||||||
) : ExploreItem {
|
) : ExploreItem {
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
@@ -63,12 +64,15 @@ sealed interface ExploreItem : ListModel {
|
|||||||
other as Source
|
other as Source
|
||||||
|
|
||||||
if (source != other.source) return false
|
if (source != other.source) return false
|
||||||
|
if (isGrid != other.isGrid) return false
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
return source.hashCode()
|
var result = source.hashCode()
|
||||||
|
result = 31 * result + isGrid.hashCode()
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -91,6 +91,12 @@ class ScrobblingSelectorBottomSheet :
|
|||||||
viewModel.onClose.observe(viewLifecycleOwner) {
|
viewModel.onClose.observe(viewLifecycleOwner) {
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
viewModel.selectedScrobblerIndex.observe(viewLifecycleOwner) { index ->
|
||||||
|
val tab = binding.tabs.getTabAt(index)
|
||||||
|
if (tab != null && !tab.isSelected) {
|
||||||
|
tab.select()
|
||||||
|
}
|
||||||
|
}
|
||||||
viewModel.searchQuery.observe(viewLifecycleOwner) {
|
viewModel.searchQuery.observe(viewLifecycleOwner) {
|
||||||
binding.headerBar.subtitle = it
|
binding.headerBar.subtitle = it
|
||||||
}
|
}
|
||||||
@@ -106,14 +112,16 @@ class ScrobblingSelectorBottomSheet :
|
|||||||
viewModel.selectedItemId.value = item.id
|
viewModel.selectedItemId.value = item.id
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRetryClick(error: Throwable) = Unit
|
override fun onRetryClick(error: Throwable) {
|
||||||
|
viewModel.retry()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onEmptyActionClick() {
|
override fun onEmptyActionClick() {
|
||||||
openSearch()
|
openSearch()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onScrolledToEnd() {
|
override fun onScrolledToEnd() {
|
||||||
viewModel.loadList(append = true)
|
viewModel.loadNextPage()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||||
|
|||||||
@@ -11,18 +11,19 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.Job
|
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.filterNotNull
|
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||||
import org.koitharu.kotatsu.list.ui.model.EmptyHint
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
|
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
|
||||||
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||||
import org.koitharu.kotatsu.scrobbling.domain.Scrobbler
|
import org.koitharu.kotatsu.scrobbling.domain.Scrobbler
|
||||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga
|
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga
|
||||||
|
import org.koitharu.kotatsu.scrobbling.ui.selector.model.ScrobblerHint
|
||||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||||
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
||||||
|
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||||
import org.koitharu.kotatsu.utils.ext.requireValue
|
import org.koitharu.kotatsu.utils.ext.requireValue
|
||||||
|
|
||||||
class ScrobblingSelectorViewModel @AssistedInject constructor(
|
class ScrobblingSelectorViewModel @AssistedInject constructor(
|
||||||
@@ -34,8 +35,9 @@ class ScrobblingSelectorViewModel @AssistedInject constructor(
|
|||||||
|
|
||||||
val selectedScrobblerIndex = MutableLiveData(0)
|
val selectedScrobblerIndex = MutableLiveData(0)
|
||||||
|
|
||||||
private val scrobblerMangaList = MutableStateFlow<List<ScrobblerManga>?>(null)
|
private val scrobblerMangaList = MutableStateFlow<List<ScrobblerManga>>(emptyList())
|
||||||
private val hasNextPage = MutableStateFlow(false)
|
private val hasNextPage = MutableStateFlow(true)
|
||||||
|
private val listError = MutableStateFlow<Throwable?>(null)
|
||||||
private var loadingJob: Job? = null
|
private var loadingJob: Job? = null
|
||||||
private var doneJob: Job? = null
|
private var doneJob: Job? = null
|
||||||
private var initJob: Job? = null
|
private var initJob: Job? = null
|
||||||
@@ -44,13 +46,24 @@ class ScrobblingSelectorViewModel @AssistedInject constructor(
|
|||||||
get() = availableScrobblers[selectedScrobblerIndex.requireValue()]
|
get() = availableScrobblers[selectedScrobblerIndex.requireValue()]
|
||||||
|
|
||||||
val content: LiveData<List<ListModel>> = combine(
|
val content: LiveData<List<ListModel>> = combine(
|
||||||
scrobblerMangaList.filterNotNull(),
|
scrobblerMangaList,
|
||||||
|
listError,
|
||||||
hasNextPage,
|
hasNextPage,
|
||||||
) { list, isHasNextPage ->
|
) { list, error, isHasNextPage ->
|
||||||
when {
|
if (list.isNotEmpty()) {
|
||||||
list.isEmpty() -> listOf(emptyResultsHint())
|
if (isHasNextPage) {
|
||||||
isHasNextPage -> list + LoadingFooter
|
list + LoadingFooter
|
||||||
else -> list
|
} else {
|
||||||
|
list
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
listOf(
|
||||||
|
when {
|
||||||
|
error != null -> errorHint(error)
|
||||||
|
isHasNextPage -> LoadingFooter
|
||||||
|
else -> emptyResultsHint()
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
|
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
|
||||||
|
|
||||||
@@ -59,7 +72,7 @@ class ScrobblingSelectorViewModel @AssistedInject constructor(
|
|||||||
val onClose = SingleLiveEvent<Unit>()
|
val onClose = SingleLiveEvent<Unit>()
|
||||||
|
|
||||||
val isEmpty: Boolean
|
val isEmpty: Boolean
|
||||||
get() = scrobblerMangaList.value.isNullOrEmpty()
|
get() = scrobblerMangaList.value.isEmpty()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initialize()
|
initialize()
|
||||||
@@ -71,22 +84,39 @@ class ScrobblingSelectorViewModel @AssistedInject constructor(
|
|||||||
loadList(append = false)
|
loadList(append = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadList(append: Boolean) {
|
fun loadNextPage() {
|
||||||
|
if (scrobblerMangaList.value.isNotEmpty() && hasNextPage.value) {
|
||||||
|
loadList(append = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun retry() {
|
||||||
|
loadingJob?.cancel()
|
||||||
|
hasNextPage.value = true
|
||||||
|
scrobblerMangaList.value = emptyList()
|
||||||
|
loadList(append = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadList(append: Boolean) {
|
||||||
if (loadingJob?.isActive == true) {
|
if (loadingJob?.isActive == true) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (append && !hasNextPage.value) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
loadingJob = launchLoadingJob(Dispatchers.Default) {
|
loadingJob = launchLoadingJob(Dispatchers.Default) {
|
||||||
val offset = if (append) scrobblerMangaList.value?.size ?: 0 else 0
|
listError.value = null
|
||||||
val list = currentScrobbler.findManga(checkNotNull(searchQuery.value), offset)
|
val offset = if (append) scrobblerMangaList.value.size else 0
|
||||||
if (!append) {
|
runCatchingCancellable {
|
||||||
scrobblerMangaList.value = list
|
currentScrobbler.findManga(checkNotNull(searchQuery.value), offset)
|
||||||
} else if (list.isNotEmpty()) {
|
}.onSuccess { list ->
|
||||||
scrobblerMangaList.value = scrobblerMangaList.value?.plus(list) ?: list
|
if (!append) {
|
||||||
|
scrobblerMangaList.value = list
|
||||||
|
} else if (list.isNotEmpty()) {
|
||||||
|
scrobblerMangaList.value = scrobblerMangaList.value + list
|
||||||
|
}
|
||||||
|
hasNextPage.value = list.isNotEmpty()
|
||||||
|
}.onFailure { error ->
|
||||||
|
error.printStackTraceDebug()
|
||||||
|
listError.value = error
|
||||||
}
|
}
|
||||||
hasNextPage.value = list.isNotEmpty()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,8 +143,8 @@ class ScrobblingSelectorViewModel @AssistedInject constructor(
|
|||||||
private fun initialize() {
|
private fun initialize() {
|
||||||
initJob?.cancel()
|
initJob?.cancel()
|
||||||
loadingJob?.cancel()
|
loadingJob?.cancel()
|
||||||
hasNextPage.value = false
|
hasNextPage.value = true
|
||||||
scrobblerMangaList.value = null
|
scrobblerMangaList.value = emptyList()
|
||||||
initJob = launchJob(Dispatchers.Default) {
|
initJob = launchJob(Dispatchers.Default) {
|
||||||
try {
|
try {
|
||||||
val info = currentScrobbler.getScrobblingInfoOrNull(manga.id)
|
val info = currentScrobbler.getScrobblingInfoOrNull(manga.id)
|
||||||
@@ -127,13 +157,22 @@ class ScrobblingSelectorViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun emptyResultsHint() = EmptyHint(
|
private fun emptyResultsHint() = ScrobblerHint(
|
||||||
icon = R.drawable.ic_empty_history,
|
icon = R.drawable.ic_empty_history,
|
||||||
textPrimary = R.string.nothing_found,
|
textPrimary = R.string.nothing_found,
|
||||||
textSecondary = R.string.text_search_holder_secondary,
|
textSecondary = R.string.text_search_holder_secondary,
|
||||||
|
error = null,
|
||||||
actionStringRes = R.string.search,
|
actionStringRes = R.string.search,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private fun errorHint(e: Throwable) = ScrobblerHint(
|
||||||
|
icon = R.drawable.ic_error_large,
|
||||||
|
textPrimary = R.string.error_occurred,
|
||||||
|
error = e,
|
||||||
|
textSecondary = 0,
|
||||||
|
actionStringRes = R.string.try_again,
|
||||||
|
)
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package org.koitharu.kotatsu.scrobbling.ui.selector.adapter
|
||||||
|
|
||||||
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
|
import org.koitharu.kotatsu.databinding.ItemEmptyHintBinding
|
||||||
|
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
import org.koitharu.kotatsu.scrobbling.ui.selector.model.ScrobblerHint
|
||||||
|
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||||
|
import org.koitharu.kotatsu.utils.ext.setTextAndVisible
|
||||||
|
import org.koitharu.kotatsu.utils.ext.textAndVisible
|
||||||
|
|
||||||
|
fun scrobblerHintAD(
|
||||||
|
listener: ListStateHolderListener,
|
||||||
|
) = adapterDelegateViewBinding<ScrobblerHint, ListModel, ItemEmptyHintBinding>(
|
||||||
|
{ inflater, parent -> ItemEmptyHintBinding.inflate(inflater, parent, false) },
|
||||||
|
) {
|
||||||
|
|
||||||
|
binding.buttonRetry.setOnClickListener {
|
||||||
|
val e = item.error
|
||||||
|
if (e != null) {
|
||||||
|
listener.onRetryClick(e)
|
||||||
|
} else {
|
||||||
|
listener.onEmptyActionClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bind {
|
||||||
|
binding.icon.setImageResource(item.icon)
|
||||||
|
binding.textPrimary.setText(item.textPrimary)
|
||||||
|
if (item.error != null) {
|
||||||
|
binding.textSecondary.textAndVisible = item.error?.getDisplayMessage(context.resources)
|
||||||
|
} else {
|
||||||
|
binding.textSecondary.setTextAndVisible(item.textSecondary)
|
||||||
|
}
|
||||||
|
binding.buttonRetry.setTextAndVisible(item.actionStringRes)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,11 +6,11 @@ import coil.ImageLoader
|
|||||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
|
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.emptyHintAD
|
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
|
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
|
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga
|
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga
|
||||||
|
import org.koitharu.kotatsu.scrobbling.ui.selector.model.ScrobblerHint
|
||||||
import kotlin.jvm.internal.Intrinsics
|
import kotlin.jvm.internal.Intrinsics
|
||||||
|
|
||||||
class ScrobblerSelectorAdapter(
|
class ScrobblerSelectorAdapter(
|
||||||
@@ -24,7 +24,7 @@ class ScrobblerSelectorAdapter(
|
|||||||
delegatesManager.addDelegate(loadingStateAD())
|
delegatesManager.addDelegate(loadingStateAD())
|
||||||
.addDelegate(scrobblingMangaAD(lifecycleOwner, coil, clickListener))
|
.addDelegate(scrobblingMangaAD(lifecycleOwner, coil, clickListener))
|
||||||
.addDelegate(loadingFooterAD())
|
.addDelegate(loadingFooterAD())
|
||||||
.addDelegate(emptyHintAD(stateHolderListener))
|
.addDelegate(scrobblerHintAD(stateHolderListener))
|
||||||
}
|
}
|
||||||
|
|
||||||
private class DiffCallback : DiffUtil.ItemCallback<ListModel>() {
|
private class DiffCallback : DiffUtil.ItemCallback<ListModel>() {
|
||||||
@@ -33,6 +33,7 @@ class ScrobblerSelectorAdapter(
|
|||||||
return when {
|
return when {
|
||||||
oldItem === newItem -> true
|
oldItem === newItem -> true
|
||||||
oldItem is ScrobblerManga && newItem is ScrobblerManga -> oldItem.id == newItem.id
|
oldItem is ScrobblerManga && newItem is ScrobblerManga -> oldItem.id == newItem.id
|
||||||
|
oldItem is ScrobblerHint && newItem is ScrobblerHint -> oldItem.textPrimary == newItem.textPrimary
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package org.koitharu.kotatsu.scrobbling.ui.selector.model
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
|
||||||
|
class ScrobblerHint(
|
||||||
|
@DrawableRes val icon: Int,
|
||||||
|
@StringRes val textPrimary: Int,
|
||||||
|
@StringRes val textSecondary: Int,
|
||||||
|
val error: Throwable?,
|
||||||
|
@StringRes val actionStringRes: Int,
|
||||||
|
) : ListModel {
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as ScrobblerHint
|
||||||
|
|
||||||
|
if (icon != other.icon) return false
|
||||||
|
if (textPrimary != other.textPrimary) return false
|
||||||
|
if (textSecondary != other.textSecondary) return false
|
||||||
|
if (error != other.error) return false
|
||||||
|
if (actionStringRes != other.actionStringRes) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = icon
|
||||||
|
result = 31 * result + textPrimary
|
||||||
|
result = 31 * result + textSecondary
|
||||||
|
result = 31 * result + (error?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + actionStringRes
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
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:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/recyclerView"
|
android:id="@+id/recyclerView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@@ -9,9 +8,8 @@
|
|||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:paddingLeft="@dimen/list_spacing"
|
android:paddingLeft="@dimen/list_spacing"
|
||||||
android:paddingRight="@dimen/list_spacing"
|
|
||||||
android:paddingTop="@dimen/grid_spacing_outer"
|
android:paddingTop="@dimen/grid_spacing_outer"
|
||||||
|
android:paddingRight="@dimen/list_spacing"
|
||||||
android:paddingBottom="@dimen/grid_spacing_outer"
|
android:paddingBottom="@dimen/grid_spacing_outer"
|
||||||
android:scrollbars="vertical"
|
android:scrollbars="vertical"
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
tools:listitem="@layout/item_explore_source_list" />
|
||||||
tools:listitem="@layout/item_explore_source" />
|
|
||||||
|
|||||||
55
app/src/main/res/layout/item_empty_hint.xml
Normal file
55
app/src/main/res/layout/item_empty_hint.xml
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
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="@dimen/margin_normal">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/icon"
|
||||||
|
android:layout_width="120dp"
|
||||||
|
android:layout_height="120dp"
|
||||||
|
android:contentDescription="@null"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:src="@drawable/ic_empty_favourites" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textPrimary"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/margin_small"
|
||||||
|
android:textAppearance="?attr/textAppearanceTitleLarge"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/icon"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="@tools:sample/lorem[3]" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textSecondary"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/margin_small"
|
||||||
|
android:layout_marginTop="@dimen/margin_small"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/icon"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/textPrimary"
|
||||||
|
tools:text="@tools:sample/lorem[15]" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_retry"
|
||||||
|
style="@style/Widget.Material3.Button.TonalButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/margin_small"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/textSecondary"
|
||||||
|
tools:text="@string/try_again"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
38
app/src/main/res/layout/item_explore_source_grid.xml
Normal file
38
app/src/main/res/layout/item_explore_source_grid.xml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?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="wrap_content"
|
||||||
|
android:background="@drawable/list_selector"
|
||||||
|
android:clipChildren="false"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="@dimen/list_spacing"
|
||||||
|
tools:layout_width="120dp">
|
||||||
|
|
||||||
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
|
android:id="@+id/imageView_icon"
|
||||||
|
android:layout_width="72dp"
|
||||||
|
android:layout_height="72dp"
|
||||||
|
android:background="?colorControlHighlight"
|
||||||
|
android:labelFor="@id/textView_title"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
app:shapeAppearance="?shapeAppearanceCornerMedium"
|
||||||
|
tools:src="@tools:sample/avatars" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/list_spacing"
|
||||||
|
android:elegantTextHeight="false"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||||
|
tools:text="@tools:sample/lorem[2]" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
<dimen name="reading_progress_text_size">10dp</dimen>
|
<dimen name="reading_progress_text_size">10dp</dimen>
|
||||||
<dimen name="reader_bar_inset_fallback">8dp</dimen>
|
<dimen name="reader_bar_inset_fallback">8dp</dimen>
|
||||||
<dimen name="scrobbling_list_spacing">12dp</dimen>
|
<dimen name="scrobbling_list_spacing">12dp</dimen>
|
||||||
|
<dimen name="explore_grid_width">120dp</dimen>
|
||||||
|
|
||||||
<dimen name="search_suggestions_manga_height">124dp</dimen>
|
<dimen name="search_suggestions_manga_height">124dp</dimen>
|
||||||
<dimen name="search_suggestions_manga_spacing">4dp</dimen>
|
<dimen name="search_suggestions_manga_spacing">4dp</dimen>
|
||||||
|
|||||||
@@ -412,4 +412,5 @@
|
|||||||
<string name="theme_name_dynamic">Dynamic</string>
|
<string name="theme_name_dynamic">Dynamic</string>
|
||||||
<string name="color_theme">Color scheme</string>
|
<string name="color_theme">Color scheme</string>
|
||||||
<string name="theme_name_october">October</string>
|
<string name="theme_name_october">October</string>
|
||||||
|
<string name="show_in_grid_view">Show in grid view</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -9,10 +9,16 @@
|
|||||||
android:key="remote_sources"
|
android:key="remote_sources"
|
||||||
android:title="@string/remote_sources" />
|
android:title="@string/remote_sources" />
|
||||||
|
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
android:defaultValue="false"
|
||||||
|
android:key="sources_grid"
|
||||||
|
android:title="@string/show_in_grid_view" />
|
||||||
|
|
||||||
<PreferenceScreen
|
<PreferenceScreen
|
||||||
android:fragment="org.koitharu.kotatsu.settings.SuggestionsSettingsFragment"
|
android:fragment="org.koitharu.kotatsu.settings.SuggestionsSettingsFragment"
|
||||||
android:key="suggestions"
|
android:key="suggestions"
|
||||||
android:title="@string/suggestions" />
|
android:title="@string/suggestions"
|
||||||
|
app:allowDividerAbove="true" />
|
||||||
|
|
||||||
<ListPreference
|
<ListPreference
|
||||||
android:entries="@array/doh_providers"
|
android:entries="@array/doh_providers"
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ buildscript {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:7.4.0'
|
classpath 'com.android.tools.build:gradle:7.4.1'
|
||||||
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.0'
|
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.10'
|
||||||
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.44'
|
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.44.2'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user