Initial adding Explore screen
This commit is contained in:
@@ -21,6 +21,7 @@ import org.koitharu.kotatsu.core.network.networkModule
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.uiModule
|
||||
import org.koitharu.kotatsu.details.detailsModule
|
||||
import org.koitharu.kotatsu.explore.exploreModule
|
||||
import org.koitharu.kotatsu.favourites.favouritesModule
|
||||
import org.koitharu.kotatsu.history.historyModule
|
||||
import org.koitharu.kotatsu.library.libraryModule
|
||||
@@ -79,6 +80,7 @@ class KotatsuApp : Application() {
|
||||
shikimoriModule,
|
||||
bookmarksModule,
|
||||
libraryModule,
|
||||
exploreModule,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.koitharu.kotatsu.base.ui.util.ActionModeDelegate
|
||||
import org.koitharu.kotatsu.base.ui.util.WindowInsetsDelegate
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
||||
|
||||
abstract class BaseActivity<B : ViewBinding> :
|
||||
AppCompatActivity(),
|
||||
|
||||
@@ -332,7 +332,8 @@ class DetailsFragment :
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) {
|
||||
binding.root.updatePadding(
|
||||
bottom = insets.bottom,
|
||||
left = insets.left,
|
||||
right = insets.right,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.koitharu.kotatsu.explore
|
||||
|
||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||
import org.koin.dsl.module
|
||||
import org.koitharu.kotatsu.explore.ui.ExploreViewModel
|
||||
|
||||
val exploreModule
|
||||
get() = module {
|
||||
viewModel { ExploreViewModel(get()) }
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package org.koitharu.kotatsu.explore.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.koin.android.ext.android.get
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseFragment
|
||||
import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner
|
||||
import org.koitharu.kotatsu.databinding.FragmentExploreBinding
|
||||
import org.koitharu.kotatsu.explore.ui.adapter.ExploreAdapter
|
||||
import org.koitharu.kotatsu.explore.ui.adapter.SourcesHeaderEventListener
|
||||
import org.koitharu.kotatsu.library.ui.adapter.LibraryAdapter
|
||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
||||
|
||||
class ExploreFragment : BaseFragment<FragmentExploreBinding>(),
|
||||
RecyclerViewOwner,
|
||||
SourcesHeaderEventListener {
|
||||
|
||||
private val viewModel by viewModel<ExploreViewModel>()
|
||||
private var exploreAdapter: ExploreAdapter? = null
|
||||
private var paddingHorizontal = 0
|
||||
|
||||
override val recyclerView: RecyclerView
|
||||
get() = binding.recyclerView
|
||||
|
||||
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): FragmentExploreBinding {
|
||||
return FragmentExploreBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
exploreAdapter = ExploreAdapter(get(), viewLifecycleOwner, this)
|
||||
with(binding.recyclerView) {
|
||||
adapter = exploreAdapter
|
||||
setHasFixedSize(true)
|
||||
val spacing = resources.getDimensionPixelOffset(R.dimen.list_spacing)
|
||||
paddingHorizontal = spacing
|
||||
}
|
||||
viewModel.items.observe(viewLifecycleOwner) {
|
||||
exploreAdapter!!.items = it
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
exploreAdapter = null
|
||||
}
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) {
|
||||
binding.root.updatePadding(
|
||||
left = insets.left,
|
||||
right = insets.right,
|
||||
)
|
||||
binding.recyclerView.updatePadding(
|
||||
left = insets.left + paddingHorizontal,
|
||||
right = insets.right + paddingHorizontal,
|
||||
bottom = insets.bottom,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onManageClick(view: View) {
|
||||
startActivity(SettingsActivity.newManageSourcesIntent(view.context))
|
||||
}
|
||||
|
||||
override fun onRetryClick(error: Throwable) = Unit
|
||||
|
||||
override fun onEmptyActionClick() = Unit
|
||||
|
||||
companion object {
|
||||
|
||||
fun newInstance() = ExploreFragment()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package org.koitharu.kotatsu.explore.ui
|
||||
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.model.getLocaleTitle
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.explore.ui.model.ExploreItem
|
||||
import org.koitharu.kotatsu.utils.ext.map
|
||||
import java.util.*
|
||||
|
||||
private const val KEY_ENABLED = "!"
|
||||
|
||||
class ExploreViewModel(
|
||||
private val settings: AppSettings,
|
||||
) : BaseViewModel() {
|
||||
|
||||
val items = MutableLiveData<List<ExploreItem>>(emptyList())
|
||||
|
||||
init {
|
||||
buildList()
|
||||
}
|
||||
|
||||
private fun buildList() {
|
||||
val sources = settings.getMangaSources(includeHidden = true)
|
||||
val hiddenSources = settings.hiddenSources
|
||||
val map = sources.groupByTo(TreeMap(LocaleKeyComparator())) {
|
||||
if (it.name !in hiddenSources) {
|
||||
KEY_ENABLED
|
||||
} else {
|
||||
it.locale
|
||||
}
|
||||
}
|
||||
val result = ArrayList<ExploreItem>(sources.size + map.size + 1)
|
||||
result += ExploreItem.Buttons
|
||||
val enabledSources = map.remove(KEY_ENABLED)
|
||||
if (!enabledSources.isNullOrEmpty()) {
|
||||
result += ExploreItem.Header(R.string.enabled_sources)
|
||||
enabledSources.mapTo(result) {
|
||||
ExploreItem.Source(
|
||||
source = it,
|
||||
summary = it.getLocaleTitle(),
|
||||
)
|
||||
}
|
||||
}
|
||||
items.value = result
|
||||
}
|
||||
|
||||
private class LocaleKeyComparator : Comparator<String?> {
|
||||
|
||||
private val deviceLocales = LocaleListCompat.getAdjustedDefault()
|
||||
.map { it.language }
|
||||
|
||||
override fun compare(a: String?, b: String?): Int {
|
||||
when {
|
||||
a == b -> return 0
|
||||
a == null -> return 1
|
||||
b == null -> return -1
|
||||
}
|
||||
val ai = deviceLocales.indexOf(a!!)
|
||||
val bi = deviceLocales.indexOf(b!!)
|
||||
return when {
|
||||
ai < 0 && bi < 0 -> a.compareTo(b)
|
||||
ai < 0 -> 1
|
||||
bi < 0 -> -1
|
||||
else -> ai.compareTo(bi)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.koitharu.kotatsu.explore.ui.adapter
|
||||
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import coil.ImageLoader
|
||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||
import org.koitharu.kotatsu.explore.ui.model.ExploreItem
|
||||
|
||||
class ExploreAdapter(
|
||||
coil: ImageLoader,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
listener: SourcesHeaderEventListener,
|
||||
) : AsyncListDifferDelegationAdapter<ExploreItem>(
|
||||
ExploreDiffCallback(),
|
||||
exploreButtonsDelegate(),
|
||||
sourceHeaderDelegate(listener),
|
||||
sourceItemDelegate(coil, lifecycleOwner),
|
||||
) {
|
||||
|
||||
init {
|
||||
delegatesManager
|
||||
.addDelegate(exploreButtonsDelegate())
|
||||
.addDelegate(sourceHeaderDelegate(listener = listener))
|
||||
.addDelegate(sourceItemDelegate(coil, lifecycleOwner))
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package org.koitharu.kotatsu.explore.ui.adapter
|
||||
|
||||
import android.view.View
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import coil.ImageLoader
|
||||
import coil.request.Disposable
|
||||
import coil.request.ImageRequest
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.databinding.ItemExploreButtonsBinding
|
||||
import org.koitharu.kotatsu.databinding.ItemExploreHeaderBinding
|
||||
import org.koitharu.kotatsu.databinding.ItemExploreSourceBinding
|
||||
import org.koitharu.kotatsu.explore.ui.model.ExploreItem
|
||||
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||
|
||||
fun exploreButtonsDelegate() = adapterDelegateViewBinding<ExploreItem.Buttons, ExploreItem, ItemExploreButtonsBinding>(
|
||||
{ layoutInflater, parent -> ItemExploreButtonsBinding.inflate(layoutInflater, parent, false) }
|
||||
) {
|
||||
|
||||
binding.localStorage.requestFocus() // stub
|
||||
|
||||
} // TODO
|
||||
|
||||
fun sourceHeaderDelegate(
|
||||
listener: SourcesHeaderEventListener,
|
||||
) = adapterDelegateViewBinding<ExploreItem.Header, ExploreItem, ItemExploreHeaderBinding>(
|
||||
{ layoutInflater, parent -> ItemExploreHeaderBinding.inflate(layoutInflater, parent, false) }
|
||||
) {
|
||||
|
||||
val listenerAdapter = View.OnClickListener {
|
||||
listener.onManageClick(itemView)
|
||||
}
|
||||
|
||||
binding.buttonMore.setOnClickListener(listenerAdapter)
|
||||
|
||||
bind {
|
||||
binding.textViewTitle.setText(R.string.remote_sources)
|
||||
}
|
||||
}
|
||||
|
||||
fun sourceItemDelegate(
|
||||
coil: ImageLoader,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
) = adapterDelegateViewBinding<ExploreItem.Source, ExploreItem, ItemExploreSourceBinding>(
|
||||
{ layoutInflater, parent -> ItemExploreSourceBinding.inflate(layoutInflater, parent, false) },
|
||||
on = { item, _, _ -> item is ExploreItem.Source }
|
||||
) {
|
||||
|
||||
var imageRequest: Disposable? = null
|
||||
|
||||
bind {
|
||||
binding.textViewTitle.text = item.source.title
|
||||
imageRequest = ImageRequest.Builder(context)
|
||||
.data(item.faviconUrl)
|
||||
.error(R.drawable.ic_favicon_fallback)
|
||||
.target(binding.imageViewCover)
|
||||
.lifecycle(lifecycleOwner)
|
||||
.enqueueWith(coil)
|
||||
}
|
||||
|
||||
onViewRecycled {
|
||||
imageRequest?.dispose()
|
||||
imageRequest = null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.koitharu.kotatsu.explore.ui.adapter
|
||||
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import org.koitharu.kotatsu.explore.ui.model.ExploreItem
|
||||
|
||||
class ExploreDiffCallback : DiffUtil.ItemCallback<ExploreItem>() {
|
||||
|
||||
override fun areItemsTheSame(oldItem: ExploreItem, newItem: ExploreItem): Boolean {
|
||||
return when {
|
||||
oldItem.javaClass != newItem.javaClass -> false
|
||||
oldItem is ExploreItem.Buttons && newItem is ExploreItem.Buttons -> {
|
||||
oldItem == newItem
|
||||
}
|
||||
oldItem is ExploreItem.Source && newItem is ExploreItem.Source -> {
|
||||
oldItem.source == newItem.source
|
||||
}
|
||||
oldItem is ExploreItem.Header && newItem is ExploreItem.Header -> {
|
||||
oldItem.titleResId == newItem.titleResId
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: ExploreItem, newItem: ExploreItem): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.koitharu.kotatsu.explore.ui.adapter
|
||||
|
||||
import android.view.View
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
|
||||
|
||||
interface SourcesHeaderEventListener : ListStateHolderListener {
|
||||
|
||||
fun onManageClick(view: View)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.koitharu.kotatsu.explore.ui.model
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.annotation.StringRes
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
|
||||
sealed interface ExploreItem {
|
||||
|
||||
object Buttons : ExploreItem
|
||||
|
||||
class Header(
|
||||
@StringRes val titleResId: Int,
|
||||
) : ExploreItem {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
other as Header
|
||||
return titleResId == other.titleResId
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = titleResId
|
||||
}
|
||||
|
||||
class Source(
|
||||
val source: MangaSource,
|
||||
val summary: String?,
|
||||
) : ExploreItem {
|
||||
|
||||
val faviconUrl: Uri
|
||||
get() = Uri.fromParts("favicon", source.name, null)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as Source
|
||||
|
||||
if (source != other.source) return false
|
||||
if (summary != other.summary) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = source.hashCode()
|
||||
result = 31 * result + summary.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,7 +3,9 @@ package org.koitharu.kotatsu.history.ui
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.commit
|
||||
import org.koitharu.kotatsu.R
|
||||
@@ -26,7 +28,11 @@ class HistoryActivity : BaseActivity<ActivityContainerBinding>() {
|
||||
}
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) {
|
||||
binding.toolbar.updatePadding(
|
||||
binding.toolbar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
leftMargin = insets.left
|
||||
rightMargin = insets.right
|
||||
}
|
||||
binding.root.updatePadding(
|
||||
left = insets.left,
|
||||
right = insets.right,
|
||||
)
|
||||
|
||||
@@ -99,9 +99,11 @@ class LibraryFragment : BaseFragment<FragmentLibraryBinding>(), LibraryListEvent
|
||||
override fun onEmptyActionClick() = Unit
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) {
|
||||
binding.recyclerView.updatePadding(
|
||||
binding.root.updatePadding(
|
||||
left = insets.left,
|
||||
right = insets.right,
|
||||
)
|
||||
binding.recyclerView.updatePadding(
|
||||
bottom = insets.bottom,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import org.koitharu.kotatsu.base.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.databinding.ActivityMainBinding
|
||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||
import org.koitharu.kotatsu.explore.ui.ExploreFragment
|
||||
import org.koitharu.kotatsu.favourites.ui.FavouritesContainerFragment
|
||||
import org.koitharu.kotatsu.library.ui.LibraryFragment
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
@@ -248,8 +249,8 @@ class MainActivity :
|
||||
binding.root.isLiftAppBarOnScroll = true // придумать лучше
|
||||
}
|
||||
R.id.nav_explore -> {
|
||||
setPrimaryFragment(FavouritesContainerFragment.newInstance())
|
||||
binding.root.isLiftAppBarOnScroll = false // --//--
|
||||
setPrimaryFragment(ExploreFragment.newInstance())
|
||||
binding.root.isLiftAppBarOnScroll = true // --//--
|
||||
}
|
||||
R.id.nav_feed -> {
|
||||
setPrimaryFragment(FeedFragment.newInstance())
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.koitharu.kotatsu.databinding.ActivitySettingsBinding
|
||||
import org.koitharu.kotatsu.main.ui.AppBarOwner
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.scrobbling.shikimori.ui.ShikimoriSettingsFragment
|
||||
import org.koitharu.kotatsu.settings.sources.SourcesSettingsFragment
|
||||
import org.koitharu.kotatsu.utils.ext.isScrolledToTop
|
||||
|
||||
class SettingsActivity :
|
||||
@@ -126,6 +127,7 @@ class SettingsActivity :
|
||||
ACTION_SOURCE -> SourceSettingsFragment.newInstance(
|
||||
intent.getSerializableExtra(EXTRA_SOURCE) as? MangaSource ?: MangaSource.LOCAL
|
||||
)
|
||||
ACTION_MANAGE_SOURCES -> SourcesSettingsFragment()
|
||||
else -> SettingsHeadersFragment()
|
||||
}
|
||||
supportFragmentManager.commit {
|
||||
@@ -150,6 +152,7 @@ class SettingsActivity :
|
||||
private const val ACTION_TRACKER = "${BuildConfig.APPLICATION_ID}.action.MANAGE_TRACKER"
|
||||
private const val ACTION_SOURCE = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SOURCE_SETTINGS"
|
||||
private const val ACTION_SHIKIMORI = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SHIKIMORI_SETTINGS"
|
||||
private const val ACTION_MANAGE_SOURCES = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SOURCES_LIST"
|
||||
private const val EXTRA_SOURCE = "source"
|
||||
|
||||
private const val HOST_SHIKIMORI_AUTH = "shikimori-auth"
|
||||
@@ -172,6 +175,10 @@ class SettingsActivity :
|
||||
Intent(context, SettingsActivity::class.java)
|
||||
.setAction(ACTION_TRACKER)
|
||||
|
||||
fun newManageSourcesIntent(context: Context) =
|
||||
Intent(context, SettingsActivity::class.java)
|
||||
.setAction(ACTION_MANAGE_SOURCES)
|
||||
|
||||
fun newSourceSettingsIntent(context: Context, source: MangaSource) =
|
||||
Intent(context, SettingsActivity::class.java)
|
||||
.setAction(ACTION_SOURCE)
|
||||
|
||||
Reference in New Issue
Block a user