Sources catalog
parent
dd898579c9
commit
b093a885c9
@ -0,0 +1,26 @@
|
|||||||
|
package org.koitharu.kotatsu.core.util
|
||||||
|
|
||||||
|
import androidx.core.os.LocaleListCompat
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.map
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class LocaleComparator : Comparator<Locale?> {
|
||||||
|
|
||||||
|
private val deviceLocales = LocaleListCompat.getAdjustedDefault()//LocaleManagerCompat.getSystemLocales(context)
|
||||||
|
.map { it.language }
|
||||||
|
.distinct()
|
||||||
|
|
||||||
|
override fun compare(a: Locale?, b: Locale?): Int {
|
||||||
|
return if (a === b) {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
val indexA = if (a == null) -1 else deviceLocales.indexOf(a.language)
|
||||||
|
val indexB = if (b == null) -1 else deviceLocales.indexOf(b.language)
|
||||||
|
if (indexA < 0 && indexB < 0) {
|
||||||
|
compareValues(a?.language, b?.language)
|
||||||
|
} else {
|
||||||
|
-2 - (indexA - indexB)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
package org.koitharu.kotatsu.settings.sources.catalog
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
|
||||||
|
sealed interface SourceCatalogItem : ListModel {
|
||||||
|
|
||||||
|
data class Source(
|
||||||
|
val source: MangaSource
|
||||||
|
) : SourceCatalogItem {
|
||||||
|
|
||||||
|
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||||
|
return other is Source && other.source == source
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Hint(
|
||||||
|
@DrawableRes val icon: Int,
|
||||||
|
@StringRes val title: Int,
|
||||||
|
@StringRes val text: Int,
|
||||||
|
) : SourceCatalogItem {
|
||||||
|
|
||||||
|
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||||
|
return other is Hint && other.title == title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,72 @@
|
|||||||
|
package org.koitharu.kotatsu.settings.sources.catalog
|
||||||
|
|
||||||
|
import androidx.core.text.buildSpannedString
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import coil.ImageLoader
|
||||||
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.model.isNsfw
|
||||||
|
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
|
||||||
|
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
|
||||||
|
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.crossfade
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.source
|
||||||
|
import org.koitharu.kotatsu.databinding.ItemEmptyHintBinding
|
||||||
|
import org.koitharu.kotatsu.databinding.ItemSourceCatalogBinding
|
||||||
|
import org.koitharu.kotatsu.settings.sources.adapter.appendNsfwLabel
|
||||||
|
|
||||||
|
fun sourceCatalogItemSourceAD(
|
||||||
|
coil: ImageLoader,
|
||||||
|
lifecycleOwner: LifecycleOwner,
|
||||||
|
listener: OnListItemClickListener<SourceCatalogItem.Source>
|
||||||
|
) = adapterDelegateViewBinding<SourceCatalogItem.Source, SourceCatalogItem, ItemSourceCatalogBinding>(
|
||||||
|
{ layoutInflater, parent ->
|
||||||
|
ItemSourceCatalogBinding.inflate(layoutInflater, parent, false)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
|
||||||
|
binding.imageViewAdd.setOnClickListener { v ->
|
||||||
|
listener.onItemClick(item, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
bind {
|
||||||
|
binding.textViewTitle.text = if (item.source.isNsfw()) {
|
||||||
|
buildSpannedString {
|
||||||
|
append(item.source.title)
|
||||||
|
append(' ')
|
||||||
|
appendNsfwLabel(context)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
item.source.title
|
||||||
|
}
|
||||||
|
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
|
||||||
|
binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
|
||||||
|
crossfade(context)
|
||||||
|
error(fallbackIcon)
|
||||||
|
placeholder(fallbackIcon)
|
||||||
|
fallback(fallbackIcon)
|
||||||
|
source(item.source)
|
||||||
|
enqueueWith(coil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sourceCatalogItemHintAD(
|
||||||
|
coil: ImageLoader,
|
||||||
|
lifecycleOwner: LifecycleOwner,
|
||||||
|
) = adapterDelegateViewBinding<SourceCatalogItem.Hint, SourceCatalogItem, ItemEmptyHintBinding>(
|
||||||
|
{ inflater, parent -> ItemEmptyHintBinding.inflate(inflater, parent, false) },
|
||||||
|
) {
|
||||||
|
|
||||||
|
binding.buttonRetry.isVisible = false
|
||||||
|
|
||||||
|
bind {
|
||||||
|
binding.icon.newImageRequest(lifecycleOwner, item.icon)?.enqueueWith(coil)
|
||||||
|
binding.textPrimary.setText(item.title)
|
||||||
|
binding.textSecondary.setTextAndVisible(item.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,103 @@
|
|||||||
|
package org.koitharu.kotatsu.settings.sources.catalog
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.core.graphics.Insets
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
|
import coil.ImageLoader
|
||||||
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
|
import com.google.android.material.tabs.TabLayout
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.model.titleResId
|
||||||
|
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||||
|
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||||
|
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.firstVisibleItemPosition
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.observe
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||||
|
import org.koitharu.kotatsu.databinding.ActivitySourcesCatalogBinding
|
||||||
|
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
||||||
|
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||||
|
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
||||||
|
import java.util.Locale
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class SourcesCatalogActivity : BaseActivity<ActivitySourcesCatalogBinding>(),
|
||||||
|
TabLayout.OnTabSelectedListener,
|
||||||
|
OnListItemClickListener<SourceCatalogItem.Source>,
|
||||||
|
AppBarOwner {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var coil: ImageLoader
|
||||||
|
|
||||||
|
override val appBar: AppBarLayout
|
||||||
|
get() = viewBinding.appbar
|
||||||
|
|
||||||
|
private val viewModel by viewModels<SourcesCatalogViewModel>()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(ActivitySourcesCatalogBinding.inflate(layoutInflater))
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
initTabs()
|
||||||
|
val sourcesAdapter = SourcesCatalogAdapter(this, coil, this)
|
||||||
|
with(viewBinding.recyclerView) {
|
||||||
|
setHasFixedSize(true)
|
||||||
|
adapter = sourcesAdapter
|
||||||
|
}
|
||||||
|
viewModel.content.observe(this, sourcesAdapter)
|
||||||
|
viewModel.onActionDone.observeEvent(
|
||||||
|
this,
|
||||||
|
ReversibleActionObserver(viewBinding.recyclerView),
|
||||||
|
)
|
||||||
|
viewModel.locale.observe(this) {
|
||||||
|
supportActionBar?.subtitle = it.getLocaleDisplayName()
|
||||||
|
}
|
||||||
|
addMenuProvider(SourcesCatalogMenuProvider(this, viewModel))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onWindowInsetsChanged(insets: Insets) {
|
||||||
|
viewBinding.root.updatePadding(
|
||||||
|
left = insets.left,
|
||||||
|
right = insets.right,
|
||||||
|
)
|
||||||
|
viewBinding.recyclerView.updatePadding(
|
||||||
|
bottom = insets.bottom + viewBinding.recyclerView.paddingTop,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemClick(item: SourceCatalogItem.Source, view: View) {
|
||||||
|
viewModel.addSource(item.source)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTabSelected(tab: TabLayout.Tab) {
|
||||||
|
viewModel.setContentType(ContentType.entries[tab.position])
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTabUnselected(tab: TabLayout.Tab) = Unit
|
||||||
|
|
||||||
|
override fun onTabReselected(tab: TabLayout.Tab) {
|
||||||
|
viewBinding.recyclerView.firstVisibleItemPosition = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initTabs() {
|
||||||
|
val tabs = viewBinding.tabs
|
||||||
|
for (type in ContentType.entries) {
|
||||||
|
val tab = tabs.newTab()
|
||||||
|
tab.setText(type.titleResId)
|
||||||
|
tabs.addTab(tab)
|
||||||
|
}
|
||||||
|
tabs.addOnTabSelectedListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String?.getLocaleDisplayName(): String {
|
||||||
|
if (this == null) {
|
||||||
|
return getString(R.string.various_languages)
|
||||||
|
}
|
||||||
|
val lc = Locale(this)
|
||||||
|
return lc.getDisplayLanguage(lc).toTitleCase(lc)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
package org.koitharu.kotatsu.settings.sources.catalog
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import coil.ImageLoader
|
||||||
|
import org.koitharu.kotatsu.core.ui.BaseListAdapter
|
||||||
|
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||||
|
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
|
||||||
|
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||||
|
|
||||||
|
class SourcesCatalogAdapter(
|
||||||
|
listener: OnListItemClickListener<SourceCatalogItem.Source>,
|
||||||
|
coil: ImageLoader,
|
||||||
|
lifecycleOwner: LifecycleOwner,
|
||||||
|
) : BaseListAdapter<SourceCatalogItem>(), FastScroller.SectionIndexer {
|
||||||
|
|
||||||
|
init {
|
||||||
|
addDelegate(ListItemType.CHAPTER, sourceCatalogItemSourceAD(coil, lifecycleOwner, listener))
|
||||||
|
addDelegate(ListItemType.HINT_EMPTY, sourceCatalogItemHintAD(coil, lifecycleOwner))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSectionText(context: Context, position: Int): CharSequence? {
|
||||||
|
return (items.getOrNull(position) as? SourceCatalogItem.Source)?.source?.title?.take(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,103 @@
|
|||||||
|
package org.koitharu.kotatsu.settings.sources.catalog
|
||||||
|
|
||||||
|
import androidx.room.InvalidationTracker
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import dagger.hilt.android.ViewModelLifecycle
|
||||||
|
import dagger.hilt.android.lifecycle.RetainedLifecycle
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.cancelAndJoin
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
|
import org.koitharu.kotatsu.core.db.TABLE_SOURCES
|
||||||
|
import org.koitharu.kotatsu.core.db.removeObserverAsync
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.lifecycleScope
|
||||||
|
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
||||||
|
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||||
|
|
||||||
|
class SourcesCatalogListProducer @AssistedInject constructor(
|
||||||
|
@Assisted private val locale: String?,
|
||||||
|
@Assisted private val contentType: ContentType,
|
||||||
|
@Assisted lifecycle: ViewModelLifecycle,
|
||||||
|
private val repository: MangaSourcesRepository,
|
||||||
|
private val database: MangaDatabase,
|
||||||
|
) : InvalidationTracker.Observer(TABLE_SOURCES), RetainedLifecycle.OnClearedListener {
|
||||||
|
|
||||||
|
private val scope = lifecycle.lifecycleScope
|
||||||
|
private var query: String = ""
|
||||||
|
val list = MutableStateFlow(emptyList<SourceCatalogItem>())
|
||||||
|
|
||||||
|
private var job = scope.launch(Dispatchers.Default) {
|
||||||
|
list.value = buildList()
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
scope.launch(Dispatchers.Default) {
|
||||||
|
database.invalidationTracker.addObserver(this@SourcesCatalogListProducer)
|
||||||
|
}
|
||||||
|
lifecycle.addOnClearedListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
database.invalidationTracker.removeObserverAsync(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onInvalidated(tables: Set<String>) {
|
||||||
|
val prevJob = job
|
||||||
|
job = scope.launch(Dispatchers.Default) {
|
||||||
|
prevJob.cancelAndJoin()
|
||||||
|
list.update { buildList() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setQuery(value: String) {
|
||||||
|
this.query = value
|
||||||
|
onInvalidated(emptySet())
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun buildList(): List<SourceCatalogItem> {
|
||||||
|
val sources = repository.getDisabledSources().toMutableList()
|
||||||
|
sources.retainAll { it.contentType == contentType && it.locale == locale }
|
||||||
|
if (query.isNotEmpty()) {
|
||||||
|
sources.retainAll { it.title.contains(query, ignoreCase = true) }
|
||||||
|
}
|
||||||
|
return if (sources.isEmpty()) {
|
||||||
|
listOf(
|
||||||
|
if (query.isEmpty()) {
|
||||||
|
SourceCatalogItem.Hint(
|
||||||
|
icon = R.drawable.ic_empty_feed,
|
||||||
|
title = R.string.no_manga_sources,
|
||||||
|
text = R.string.no_manga_sources_catalog_text,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
SourceCatalogItem.Hint(
|
||||||
|
icon = R.drawable.ic_empty_feed,
|
||||||
|
title = R.string.nothing_found,
|
||||||
|
text = R.string.no_manga_sources_found,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
sources.sortBy { it.title }
|
||||||
|
sources.map {
|
||||||
|
SourceCatalogItem.Source(
|
||||||
|
source = it,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AssistedFactory
|
||||||
|
interface Factory {
|
||||||
|
|
||||||
|
fun create(
|
||||||
|
locale: String?,
|
||||||
|
contentType: ContentType,
|
||||||
|
lifecycle: ViewModelLifecycle,
|
||||||
|
): SourcesCatalogListProducer
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,74 @@
|
|||||||
|
package org.koitharu.kotatsu.settings.sources.catalog
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuInflater
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import androidx.appcompat.widget.PopupMenu
|
||||||
|
import androidx.appcompat.widget.SearchView
|
||||||
|
import androidx.core.view.MenuProvider
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
||||||
|
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
||||||
|
|
||||||
|
class SourcesCatalogMenuProvider(
|
||||||
|
private val activity: Activity,
|
||||||
|
private val viewModel: SourcesCatalogViewModel,
|
||||||
|
) : MenuProvider,
|
||||||
|
MenuItem.OnActionExpandListener,
|
||||||
|
SearchView.OnQueryTextListener {
|
||||||
|
|
||||||
|
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||||
|
menuInflater.inflate(R.menu.opt_sources_catalog, menu)
|
||||||
|
val searchMenuItem = menu.findItem(R.id.action_search)
|
||||||
|
searchMenuItem.setOnActionExpandListener(this)
|
||||||
|
val searchView = searchMenuItem.actionView as SearchView
|
||||||
|
searchView.setOnQueryTextListener(this)
|
||||||
|
searchView.setIconifiedByDefault(false)
|
||||||
|
searchView.queryHint = searchMenuItem.title
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
|
||||||
|
R.id.action_locales -> {
|
||||||
|
showLocalesMenu()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||||
|
(activity as? AppBarOwner)?.appBar?.setExpanded(false, true)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||||
|
(item.actionView as SearchView).setQuery("", false)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onQueryTextSubmit(query: String?): Boolean = false
|
||||||
|
|
||||||
|
override fun onQueryTextChange(newText: String?): Boolean {
|
||||||
|
viewModel.performSearch(newText.orEmpty())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showLocalesMenu() {
|
||||||
|
val locales = viewModel.locales
|
||||||
|
val anchor: View = (activity as AppBarOwner).appBar.let {
|
||||||
|
it.findViewById<View?>(R.id.toolbar) ?: it
|
||||||
|
}
|
||||||
|
val menu = PopupMenu(activity, anchor)
|
||||||
|
for ((i, lc) in locales.withIndex()) {
|
||||||
|
val title = lc?.getDisplayLanguage(lc)?.toTitleCase(lc) ?: activity.getString(R.string.various_languages)
|
||||||
|
menu.menu.add(Menu.NONE, Menu.NONE, i, title)
|
||||||
|
}
|
||||||
|
menu.setOnMenuItemClickListener {
|
||||||
|
viewModel.setLocale(locales.getOrNull(it.order)?.language)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
menu.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,79 @@
|
|||||||
|
package org.koitharu.kotatsu.settings.sources.catalog
|
||||||
|
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import dagger.hilt.android.internal.lifecycle.RetainedLifecycleImpl
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||||
|
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
||||||
|
import org.koitharu.kotatsu.core.util.LocaleComparator
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.call
|
||||||
|
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
||||||
|
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||||
|
import java.util.Locale
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class SourcesCatalogViewModel @Inject constructor(
|
||||||
|
private val repository: MangaSourcesRepository,
|
||||||
|
private val listProducerFactory: SourcesCatalogListProducer.Factory,
|
||||||
|
) : BaseViewModel() {
|
||||||
|
|
||||||
|
private val lifecycle = RetainedLifecycleImpl()
|
||||||
|
val onActionDone = MutableEventFlow<ReversibleAction>()
|
||||||
|
val contentType = MutableStateFlow(ContentType.entries.first())
|
||||||
|
val locales = getLocalesImpl()
|
||||||
|
val locale = MutableStateFlow(locales.firstOrNull()?.language)
|
||||||
|
|
||||||
|
private val listProducer: StateFlow<SourcesCatalogListProducer?> = combine(
|
||||||
|
locale,
|
||||||
|
contentType,
|
||||||
|
) { lc, type ->
|
||||||
|
listProducerFactory.create(lc, type, lifecycle)
|
||||||
|
}.stateIn(viewModelScope, SharingStarted.Eagerly, null)
|
||||||
|
|
||||||
|
val content = listProducer.flatMapLatest {
|
||||||
|
it?.list ?: emptyFlow()
|
||||||
|
}.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
lifecycle.dispatchOnCleared()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun performSearch(query: String) {
|
||||||
|
listProducer.value?.setQuery(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setLocale(value: String?) {
|
||||||
|
locale.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setContentType(value: ContentType) {
|
||||||
|
contentType.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addSource(source: MangaSource) {
|
||||||
|
launchJob(Dispatchers.Default) {
|
||||||
|
val rollback = repository.setSourceEnabled(source, true)
|
||||||
|
onActionDone.call(ReversibleAction(R.string.source_enabled, rollback))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getLocalesImpl(): List<Locale?> {
|
||||||
|
return repository.allMangaSources
|
||||||
|
.mapToSet { it.locale?.let(::Locale) }
|
||||||
|
.sortedWith(LocaleComparator())
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".settings.sources.catalog.SourcesCatalogActivity">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:id="@+id/appbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fitsSystemWindows="true">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize" />
|
||||||
|
|
||||||
|
<com.google.android.material.tabs.TabLayout
|
||||||
|
android:id="@+id/tabs"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:tabGravity="start"
|
||||||
|
app:tabMode="scrollable" />
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/layout_list"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
|
||||||
|
|
||||||
|
<org.koitharu.kotatsu.core.ui.list.fastscroll.FastScrollRecyclerView
|
||||||
|
android:id="@+id/recyclerView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:orientation="vertical"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
|
tools:listitem="@layout/item_source_config" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
<?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="?android:windowBackground"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:minHeight="?listPreferredItemHeightSmall"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingVertical="@dimen/margin_small"
|
||||||
|
android:paddingStart="?listPreferredItemPaddingStart"
|
||||||
|
android:paddingEnd="?listPreferredItemPaddingEnd">
|
||||||
|
|
||||||
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
|
android:id="@+id/imageView_icon"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:background="?colorControlHighlight"
|
||||||
|
android:labelFor="@id/textView_title"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
app:shapeAppearance="?shapeAppearanceCornerSmall"
|
||||||
|
tools:src="@tools:sample/avatars" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView_title"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="?android:listPreferredItemPaddingStart"
|
||||||
|
android:layout_marginEnd="?android:listPreferredItemPaddingEnd"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textAppearance="?attr/textAppearanceTitleSmall"
|
||||||
|
tools:text="@tools:sample/lorem[15]" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/imageView_add"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?selectableItemBackgroundBorderless"
|
||||||
|
android:contentDescription="@string/add"
|
||||||
|
android:padding="@dimen/margin_small"
|
||||||
|
android:scaleType="center"
|
||||||
|
android:src="@drawable/ic_add"
|
||||||
|
android:tooltipText="@string/add" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_locales"
|
||||||
|
android:icon="@drawable/ic_expand_more"
|
||||||
|
android:title="@string/languages"
|
||||||
|
app:showAsAction="ifRoom" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_search"
|
||||||
|
android:icon="?actionModeWebSearchDrawable"
|
||||||
|
android:title="@string/search"
|
||||||
|
app:actionViewClass="androidx.appcompat.widget.SearchView"
|
||||||
|
app:showAsAction="ifRoom|collapseActionView" />
|
||||||
|
|
||||||
|
</menu>
|
||||||
Loading…
Reference in New Issue