Option to move manga source to top
This commit is contained in:
@@ -6,6 +6,7 @@ import androidx.room.InvalidationTracker
|
|||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
import androidx.room.migration.Migration
|
import androidx.room.migration.Migration
|
||||||
|
import kotlinx.coroutines.CoroutineStart
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -118,7 +119,7 @@ fun MangaDatabase(context: Context): MangaDatabase = Room
|
|||||||
fun InvalidationTracker.removeObserverAsync(observer: InvalidationTracker.Observer) {
|
fun InvalidationTracker.removeObserverAsync(observer: InvalidationTracker.Observer) {
|
||||||
val scope = processLifecycleScope
|
val scope = processLifecycleScope
|
||||||
if (scope.isActive) {
|
if (scope.isActive) {
|
||||||
processLifecycleScope.launch(Dispatchers.Default) {
|
processLifecycleScope.launch(Dispatchers.Default, CoroutineStart.ATOMIC) {
|
||||||
removeObserver(observer)
|
removeObserver(observer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,3 +6,4 @@ const val TABLE_TAGS = "tags"
|
|||||||
const val TABLE_FAVOURITE_CATEGORIES = "favourite_categories"
|
const val TABLE_FAVOURITE_CATEGORIES = "favourite_categories"
|
||||||
const val TABLE_HISTORY = "history"
|
const val TABLE_HISTORY = "history"
|
||||||
const val TABLE_MANGA_TAGS = "manga_tags"
|
const val TABLE_MANGA_TAGS = "manga_tags"
|
||||||
|
const val TABLE_SOURCES = "sources"
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ package org.koitharu.kotatsu.core.db.entity
|
|||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
|
import org.koitharu.kotatsu.core.db.TABLE_SOURCES
|
||||||
|
|
||||||
@Entity(
|
@Entity(
|
||||||
tableName = "sources",
|
tableName = TABLE_SOURCES,
|
||||||
)
|
)
|
||||||
data class MangaSourceEntity(
|
data class MangaSourceEntity(
|
||||||
@PrimaryKey(autoGenerate = false)
|
@PrimaryKey(autoGenerate = false)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.koitharu.kotatsu.core.model
|
package org.koitharu.kotatsu.core.model
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@@ -15,3 +16,5 @@ fun MangaSource(name: String): MangaSource {
|
|||||||
}
|
}
|
||||||
return MangaSource.DUMMY
|
return MangaSource.DUMMY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun MangaSource.isNsfw() = contentType == ContentType.HENTAI
|
||||||
|
|||||||
@@ -28,7 +28,10 @@ class NewSourcesDialogFragment :
|
|||||||
|
|
||||||
private val viewModel by viewModels<NewSourcesViewModel>()
|
private val viewModel by viewModels<NewSourcesViewModel>()
|
||||||
|
|
||||||
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): DialogOnboardBinding {
|
override fun onCreateViewBinding(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
): DialogOnboardBinding {
|
||||||
return DialogOnboardBinding.inflate(inflater, container, false)
|
return DialogOnboardBinding.inflate(inflater, container, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,6 +57,8 @@ class NewSourcesDialogFragment :
|
|||||||
|
|
||||||
override fun onItemSettingsClick(item: SourceConfigItem.SourceItem) = Unit
|
override fun onItemSettingsClick(item: SourceConfigItem.SourceItem) = Unit
|
||||||
|
|
||||||
|
override fun onItemLiftClick(item: SourceConfigItem.SourceItem) = Unit
|
||||||
|
|
||||||
override fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean) {
|
override fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean) {
|
||||||
viewModel.onItemEnabledChanged(item, isEnabled)
|
viewModel.onItemEnabledChanged(item, isEnabled)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import android.content.DialogInterface
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
@@ -13,7 +12,6 @@ import org.koitharu.kotatsu.R
|
|||||||
import org.koitharu.kotatsu.core.ui.AlertDialogFragment
|
import org.koitharu.kotatsu.core.ui.AlertDialogFragment
|
||||||
import org.koitharu.kotatsu.core.util.ext.observe
|
import org.koitharu.kotatsu.core.util.ext.observe
|
||||||
import org.koitharu.kotatsu.core.util.ext.showAllowStateLoss
|
import org.koitharu.kotatsu.core.util.ext.showAllowStateLoss
|
||||||
import org.koitharu.kotatsu.core.util.ext.withArgs
|
|
||||||
import org.koitharu.kotatsu.databinding.DialogOnboardBinding
|
import org.koitharu.kotatsu.databinding.DialogOnboardBinding
|
||||||
import org.koitharu.kotatsu.settings.onboard.adapter.SourceLocaleListener
|
import org.koitharu.kotatsu.settings.onboard.adapter.SourceLocaleListener
|
||||||
import org.koitharu.kotatsu.settings.onboard.adapter.SourceLocalesAdapter
|
import org.koitharu.kotatsu.settings.onboard.adapter.SourceLocalesAdapter
|
||||||
@@ -25,14 +23,6 @@ class OnboardDialogFragment :
|
|||||||
DialogInterface.OnClickListener, SourceLocaleListener {
|
DialogInterface.OnClickListener, SourceLocaleListener {
|
||||||
|
|
||||||
private val viewModel by viewModels<OnboardViewModel>()
|
private val viewModel by viewModels<OnboardViewModel>()
|
||||||
private var isWelcome: Boolean = false
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
arguments?.run {
|
|
||||||
isWelcome = getBoolean(ARG_WELCOME, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateViewBinding(
|
override fun onCreateViewBinding(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
@@ -43,11 +33,7 @@ class OnboardDialogFragment :
|
|||||||
super.onBuildDialog(builder)
|
super.onBuildDialog(builder)
|
||||||
.setPositiveButton(R.string.done, this)
|
.setPositiveButton(R.string.done, this)
|
||||||
.setCancelable(false)
|
.setCancelable(false)
|
||||||
if (isWelcome) {
|
builder.setTitle(R.string.welcome)
|
||||||
builder.setTitle(R.string.welcome)
|
|
||||||
} else {
|
|
||||||
builder.setTitle(R.string.remote_sources)
|
|
||||||
}
|
|
||||||
return builder
|
return builder
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,11 +41,7 @@ class OnboardDialogFragment :
|
|||||||
super.onViewBindingCreated(binding, savedInstanceState)
|
super.onViewBindingCreated(binding, savedInstanceState)
|
||||||
val adapter = SourceLocalesAdapter(this)
|
val adapter = SourceLocalesAdapter(this)
|
||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
if (isWelcome) {
|
binding.textViewTitle.setText(R.string.onboard_text)
|
||||||
binding.textViewTitle.setText(R.string.onboard_text)
|
|
||||||
} else {
|
|
||||||
binding.textViewTitle.isVisible = false
|
|
||||||
}
|
|
||||||
viewModel.list.observe(viewLifecycleOwner, adapter)
|
viewModel.list.observe(viewLifecycleOwner, adapter)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,14 +58,7 @@ class OnboardDialogFragment :
|
|||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val TAG = "OnboardDialog"
|
private const val TAG = "OnboardDialog"
|
||||||
private const val ARG_WELCOME = "welcome"
|
|
||||||
|
|
||||||
fun show(fm: FragmentManager) = OnboardDialogFragment().show(fm, TAG)
|
fun show(fm: FragmentManager) = OnboardDialogFragment().showAllowStateLoss(fm, TAG)
|
||||||
|
|
||||||
fun showWelcome(fm: FragmentManager) {
|
|
||||||
OnboardDialogFragment().withArgs(1) {
|
|
||||||
putBoolean(ARG_WELCOME, true)
|
|
||||||
}.showAllowStateLoss(fm, TAG)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
package org.koitharu.kotatsu.settings.sources
|
||||||
|
|
||||||
|
import androidx.core.os.LocaleListCompat
|
||||||
|
import androidx.room.InvalidationTracker
|
||||||
|
import dagger.hilt.android.ViewModelLifecycle
|
||||||
|
import dagger.hilt.android.scopes.ViewModelScoped
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.cancelAndJoin
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.db.TABLE_SOURCES
|
||||||
|
import org.koitharu.kotatsu.core.model.getLocaleTitle
|
||||||
|
import org.koitharu.kotatsu.core.model.isNsfw
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.core.util.AlphanumComparator
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.lifecycleScope
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.map
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.toEnumSet
|
||||||
|
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
||||||
|
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.TreeMap
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ViewModelScoped
|
||||||
|
class SourcesListProducer @Inject constructor(
|
||||||
|
lifecycle: ViewModelLifecycle,
|
||||||
|
private val repository: MangaSourcesRepository,
|
||||||
|
private val settings: AppSettings,
|
||||||
|
) : InvalidationTracker.Observer(TABLE_SOURCES) {
|
||||||
|
|
||||||
|
private val scope = lifecycle.lifecycleScope
|
||||||
|
private var query: String = ""
|
||||||
|
private val expanded = HashSet<String?>()
|
||||||
|
val list = MutableStateFlow(emptyList<SourceConfigItem>())
|
||||||
|
|
||||||
|
private var job = scope.launch(Dispatchers.Default) {
|
||||||
|
list.value = buildList()
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
settings.observe()
|
||||||
|
.filter { it == AppSettings.KEY_TIPS_CLOSED || it == AppSettings.KEY_DISABLE_NSFW }
|
||||||
|
.flowOn(Dispatchers.Default)
|
||||||
|
.onEach { onInvalidated(emptySet()) }
|
||||||
|
.launchIn(scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun expandCollapse(group: String?) {
|
||||||
|
if (!expanded.remove(group)) {
|
||||||
|
expanded.add(group)
|
||||||
|
}
|
||||||
|
onInvalidated(emptySet())
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun buildList(): List<SourceConfigItem> {
|
||||||
|
val allSources = repository.allMangaSources
|
||||||
|
val enabledSources = repository.getEnabledSources()
|
||||||
|
val isNsfwDisabled = settings.isNsfwContentDisabled
|
||||||
|
val withTip = settings.isTipEnabled(TIP_REORDER)
|
||||||
|
val enabledSet = enabledSources.toEnumSet()
|
||||||
|
if (query.isNotEmpty()) {
|
||||||
|
return allSources.mapNotNull {
|
||||||
|
if (!it.title.contains(query, ignoreCase = true)) {
|
||||||
|
return@mapNotNull null
|
||||||
|
}
|
||||||
|
SourceConfigItem.SourceItem(
|
||||||
|
source = it,
|
||||||
|
summary = it.getLocaleTitle(),
|
||||||
|
isEnabled = it in enabledSet,
|
||||||
|
isDraggable = false,
|
||||||
|
isAvailable = !isNsfwDisabled || !it.isNsfw(),
|
||||||
|
)
|
||||||
|
}.ifEmpty {
|
||||||
|
listOf(SourceConfigItem.EmptySearchResult)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val map = allSources.groupByTo(TreeMap(LocaleKeyComparator())) {
|
||||||
|
if (it in enabledSet) {
|
||||||
|
KEY_ENABLED
|
||||||
|
} else {
|
||||||
|
it.locale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
map.remove(KEY_ENABLED)
|
||||||
|
val result = ArrayList<SourceConfigItem>(allSources.size + map.size + 2)
|
||||||
|
if (enabledSources.isNotEmpty()) {
|
||||||
|
result += SourceConfigItem.Header(R.string.enabled_sources)
|
||||||
|
if (withTip) {
|
||||||
|
result += SourceConfigItem.Tip(
|
||||||
|
TIP_REORDER,
|
||||||
|
R.drawable.ic_tap_reorder,
|
||||||
|
R.string.sources_reorder_tip,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
enabledSources.mapTo(result) {
|
||||||
|
SourceConfigItem.SourceItem(
|
||||||
|
source = it,
|
||||||
|
summary = it.getLocaleTitle(),
|
||||||
|
isEnabled = true,
|
||||||
|
isDraggable = true,
|
||||||
|
isAvailable = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (enabledSources.size != allSources.size) {
|
||||||
|
result += SourceConfigItem.Header(R.string.available_sources)
|
||||||
|
val comparator = compareBy<MangaSource, String>(AlphanumComparator()) { it.name }
|
||||||
|
for ((key, list) in map) {
|
||||||
|
list.sortWith(comparator)
|
||||||
|
val isExpanded = key in expanded
|
||||||
|
result += SourceConfigItem.LocaleGroup(
|
||||||
|
localeId = key,
|
||||||
|
title = getLocaleTitle(key),
|
||||||
|
isExpanded = isExpanded,
|
||||||
|
)
|
||||||
|
if (isExpanded) {
|
||||||
|
list.mapTo(result) {
|
||||||
|
SourceConfigItem.SourceItem(
|
||||||
|
source = it,
|
||||||
|
summary = null,
|
||||||
|
isEnabled = false,
|
||||||
|
isDraggable = false,
|
||||||
|
isAvailable = !isNsfwDisabled || !it.isNsfw(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private fun getLocaleTitle(localeKey: String?): String? {
|
||||||
|
val locale = Locale(localeKey ?: return null)
|
||||||
|
return locale.getDisplayLanguage(locale).toTitleCase(locale)
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val KEY_ENABLED = "!"
|
||||||
|
const val TIP_REORDER = "src_reorder"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,7 +27,6 @@ import org.koitharu.kotatsu.core.util.ext.observeEvent
|
|||||||
import org.koitharu.kotatsu.databinding.FragmentSettingsSourcesBinding
|
import org.koitharu.kotatsu.databinding.FragmentSettingsSourcesBinding
|
||||||
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
||||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
import org.koitharu.kotatsu.settings.SettingsActivity
|
||||||
import org.koitharu.kotatsu.settings.onboard.OnboardDialogFragment
|
|
||||||
import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigAdapter
|
import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigAdapter
|
||||||
import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigListener
|
import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigListener
|
||||||
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
|
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
|
||||||
@@ -56,7 +55,10 @@ class SourcesManageFragment :
|
|||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
) = FragmentSettingsSourcesBinding.inflate(inflater, container, false)
|
) = FragmentSettingsSourcesBinding.inflate(inflater, container, false)
|
||||||
|
|
||||||
override fun onViewBindingCreated(binding: FragmentSettingsSourcesBinding, savedInstanceState: Bundle?) {
|
override fun onViewBindingCreated(
|
||||||
|
binding: FragmentSettingsSourcesBinding,
|
||||||
|
savedInstanceState: Bundle?,
|
||||||
|
) {
|
||||||
super.onViewBindingCreated(binding, savedInstanceState)
|
super.onViewBindingCreated(binding, savedInstanceState)
|
||||||
val sourcesAdapter = SourceConfigAdapter(this, coil, viewLifecycleOwner)
|
val sourcesAdapter = SourceConfigAdapter(this, coil, viewLifecycleOwner)
|
||||||
with(binding.recyclerView) {
|
with(binding.recyclerView) {
|
||||||
@@ -67,7 +69,10 @@ class SourcesManageFragment :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
viewModel.content.observe(viewLifecycleOwner, sourcesAdapter)
|
viewModel.content.observe(viewLifecycleOwner, sourcesAdapter)
|
||||||
viewModel.onActionDone.observeEvent(viewLifecycleOwner, ReversibleActionObserver(binding.recyclerView))
|
viewModel.onActionDone.observeEvent(
|
||||||
|
viewLifecycleOwner,
|
||||||
|
ReversibleActionObserver(binding.recyclerView)
|
||||||
|
)
|
||||||
addMenuProvider(SourcesMenuProvider())
|
addMenuProvider(SourcesMenuProvider())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,6 +99,10 @@ class SourcesManageFragment :
|
|||||||
(activity as? SettingsActivity)?.openFragment(fragment, false)
|
(activity as? SettingsActivity)?.openFragment(fragment, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onItemLiftClick(item: SourceConfigItem.SourceItem) {
|
||||||
|
viewModel.bringToTop(item.source)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean) {
|
override fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean) {
|
||||||
viewModel.setEnabled(item.source, isEnabled)
|
viewModel.setEnabled(item.source, isEnabled)
|
||||||
}
|
}
|
||||||
@@ -127,11 +136,6 @@ class SourcesManageFragment :
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.action_locales -> {
|
|
||||||
OnboardDialogFragment.show(childFragmentManager)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.action_no_nsfw -> {
|
R.id.action_no_nsfw -> {
|
||||||
settings.isNsfwContentDisabled = !menuItem.isChecked
|
settings.isNsfwContentDisabled = !menuItem.isChecked
|
||||||
true
|
true
|
||||||
@@ -181,7 +185,7 @@ class SourcesManageFragment :
|
|||||||
target: RecyclerView.ViewHolder,
|
target: RecyclerView.ViewHolder,
|
||||||
toPos: Int,
|
toPos: Int,
|
||||||
x: Int,
|
x: Int,
|
||||||
y: Int
|
y: Int,
|
||||||
) {
|
) {
|
||||||
super.onMoved(recyclerView, viewHolder, fromPos, target, toPos, x, y)
|
super.onMoved(recyclerView, viewHolder, fromPos, target, toPos, x, y)
|
||||||
viewModel.reorderSources(fromPos, toPos)
|
viewModel.reorderSources(fromPos, toPos)
|
||||||
@@ -196,7 +200,10 @@ class SourcesManageFragment :
|
|||||||
target.bindingAdapterPosition,
|
target.bindingAdapterPosition,
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun getDragDirs(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
|
override fun getDragDirs(
|
||||||
|
recyclerView: RecyclerView,
|
||||||
|
viewHolder: RecyclerView.ViewHolder,
|
||||||
|
): Int {
|
||||||
val item = viewHolder.getItem(SourceConfigItem.SourceItem::class.java)
|
val item = viewHolder.getItem(SourceConfigItem.SourceItem::class.java)
|
||||||
return if (item != null && item.isDraggable) {
|
return if (item != null && item.isDraggable) {
|
||||||
super.getDragDirs(recyclerView, viewHolder)
|
super.getDragDirs(recyclerView, viewHolder)
|
||||||
@@ -205,7 +212,10 @@ class SourcesManageFragment :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getSwipeDirs(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
|
override fun getSwipeDirs(
|
||||||
|
recyclerView: RecyclerView,
|
||||||
|
viewHolder: RecyclerView.ViewHolder,
|
||||||
|
): Int {
|
||||||
val item = viewHolder.getItem(SourceConfigItem.Tip::class.java)
|
val item = viewHolder.getItem(SourceConfigItem.Tip::class.java)
|
||||||
return if (item != null) {
|
return if (item != null) {
|
||||||
super.getSwipeDirs(recyclerView, viewHolder)
|
super.getSwipeDirs(recyclerView, viewHolder)
|
||||||
|
|||||||
@@ -1,88 +1,59 @@
|
|||||||
package org.koitharu.kotatsu.settings.sources
|
package org.koitharu.kotatsu.settings.sources
|
||||||
|
|
||||||
import androidx.annotation.CheckResult
|
|
||||||
import androidx.core.os.LocaleListCompat
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.cancelAndJoin
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.yield
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.model.getLocaleTitle
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
|
import org.koitharu.kotatsu.core.db.removeObserverAsync
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
|
||||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||||
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
||||||
import org.koitharu.kotatsu.core.util.AlphanumComparator
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||||
import org.koitharu.kotatsu.core.util.ext.call
|
import org.koitharu.kotatsu.core.util.ext.call
|
||||||
import org.koitharu.kotatsu.core.util.ext.map
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.toEnumSet
|
|
||||||
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
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.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.util.move
|
import org.koitharu.kotatsu.parsers.util.move
|
||||||
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
|
||||||
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
|
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
|
||||||
import java.util.Locale
|
|
||||||
import java.util.TreeMap
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
private const val KEY_ENABLED = "!"
|
|
||||||
private const val TIP_REORDER = "src_reorder"
|
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class SourcesManageViewModel @Inject constructor(
|
class SourcesManageViewModel @Inject constructor(
|
||||||
|
private val database: MangaDatabase,
|
||||||
private val settings: AppSettings,
|
private val settings: AppSettings,
|
||||||
private val repository: MangaSourcesRepository,
|
private val repository: MangaSourcesRepository,
|
||||||
|
private val listProducer: SourcesListProducer,
|
||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
private val expandedGroups = MutableStateFlow(emptySet<String?>())
|
val content = listProducer.list
|
||||||
private var searchQuery = MutableStateFlow<String?>(null)
|
|
||||||
private var reorderJob: Job? = null
|
|
||||||
val content = MutableStateFlow<List<SourceConfigItem>>(emptyList())
|
|
||||||
val onActionDone = MutableEventFlow<ReversibleAction>()
|
val onActionDone = MutableEventFlow<ReversibleAction>()
|
||||||
|
private var commitJob: Job? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
launchJob(Dispatchers.Default) {
|
launchJob(Dispatchers.Default) {
|
||||||
combine(
|
database.invalidationTracker.addObserver(listProducer)
|
||||||
repository.observeEnabledSources(),
|
|
||||||
expandedGroups,
|
|
||||||
searchQuery,
|
|
||||||
observeTip(),
|
|
||||||
settings.observeAsFlow(AppSettings.KEY_DISABLE_NSFW) { isNsfwContentDisabled },
|
|
||||||
) { sources, groups, query, tip, noNsfw ->
|
|
||||||
buildList(sources, groups, query, tip, noNsfw)
|
|
||||||
}.collectLatest {
|
|
||||||
reorderJob?.join()
|
|
||||||
content.value = it
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
database.invalidationTracker.removeObserverAsync(listProducer)
|
||||||
|
}
|
||||||
|
|
||||||
fun reorderSources(oldPos: Int, newPos: Int) {
|
fun reorderSources(oldPos: Int, newPos: Int) {
|
||||||
val snapshot = content.value.toMutableList()
|
val snapshot = content.value.toMutableList()
|
||||||
val prevJob = reorderJob
|
if ((snapshot[oldPos] as? SourceConfigItem.SourceItem)?.isDraggable != true) {
|
||||||
reorderJob = launchJob(Dispatchers.Default) {
|
return
|
||||||
if ((snapshot[oldPos] as? SourceConfigItem.SourceItem)?.isDraggable != true) {
|
|
||||||
return@launchJob
|
|
||||||
}
|
|
||||||
if ((snapshot[newPos] as? SourceConfigItem.SourceItem)?.isDraggable != true) {
|
|
||||||
return@launchJob
|
|
||||||
}
|
|
||||||
snapshot.move(oldPos, newPos)
|
|
||||||
content.value = snapshot
|
|
||||||
prevJob?.join()
|
|
||||||
val newSourcesList = snapshot.mapNotNull { x ->
|
|
||||||
if (x is SourceConfigItem.SourceItem && x.isDraggable) {
|
|
||||||
x.source
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
repository.setPositions(newSourcesList)
|
|
||||||
}
|
}
|
||||||
|
if ((snapshot[newPos] as? SourceConfigItem.SourceItem)?.isDraggable != true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
snapshot.move(oldPos, newPos)
|
||||||
|
content.value = snapshot
|
||||||
|
commit(snapshot)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun canReorder(oldPos: Int, newPos: Int): Boolean {
|
fun canReorder(oldPos: Int, newPos: Int): Boolean {
|
||||||
@@ -100,6 +71,32 @@ class SourcesManageViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun bringToTop(source: MangaSource) {
|
||||||
|
var oldPos = -1
|
||||||
|
var newPos = -1
|
||||||
|
val snapshot = content.value
|
||||||
|
for ((i, x) in snapshot.withIndex()) {
|
||||||
|
if (x !is SourceConfigItem.SourceItem) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (newPos == -1) {
|
||||||
|
newPos = i
|
||||||
|
}
|
||||||
|
if (x.source == source) {
|
||||||
|
oldPos = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Suppress("KotlinConstantConditions")
|
||||||
|
if (oldPos != -1 && newPos != -1) {
|
||||||
|
reorderSources(oldPos, newPos)
|
||||||
|
val revert = ReversibleAction(R.string.moved_to_top) {
|
||||||
|
reorderSources(newPos, oldPos)
|
||||||
|
}
|
||||||
|
onActionDone.call(revert)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun disableAll() {
|
fun disableAll() {
|
||||||
launchJob(Dispatchers.Default) {
|
launchJob(Dispatchers.Default) {
|
||||||
repository.disableAllSources()
|
repository.disableAllSources()
|
||||||
@@ -107,16 +104,11 @@ class SourcesManageViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun expandOrCollapse(headerId: String?) {
|
fun expandOrCollapse(headerId: String?) {
|
||||||
val expanded = expandedGroups.value
|
listProducer.expandCollapse(headerId)
|
||||||
expandedGroups.value = if (headerId in expanded) {
|
|
||||||
expanded - headerId
|
|
||||||
} else {
|
|
||||||
expanded + headerId
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun performSearch(query: String?) {
|
fun performSearch(query: String?) {
|
||||||
searchQuery.value = query?.trim()
|
listProducer.setQuery(query?.trim().orEmpty())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onTipClosed(item: SourceConfigItem.Tip) {
|
fun onTipClosed(item: SourceConfigItem.Tip) {
|
||||||
@@ -125,113 +117,20 @@ class SourcesManageViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@CheckResult
|
private fun commit(snapshot: List<SourceConfigItem>) {
|
||||||
private fun buildList(
|
val prevJob = commitJob
|
||||||
enabledSources: List<MangaSource>,
|
commitJob = launchJob {
|
||||||
expanded: Set<String?>,
|
prevJob?.cancelAndJoin()
|
||||||
query: String?,
|
delay(500)
|
||||||
withTip: Boolean,
|
val newSourcesList = snapshot.mapNotNull { x ->
|
||||||
isNsfwDisabled: Boolean,
|
if (x is SourceConfigItem.SourceItem && x.isDraggable) {
|
||||||
): List<SourceConfigItem> {
|
x.source
|
||||||
val allSources = repository.allMangaSources
|
} else {
|
||||||
val enabledSet = enabledSources.toEnumSet()
|
null
|
||||||
if (!query.isNullOrEmpty()) {
|
|
||||||
return allSources.mapNotNull {
|
|
||||||
if (!it.title.contains(query, ignoreCase = true)) {
|
|
||||||
return@mapNotNull null
|
|
||||||
}
|
|
||||||
SourceConfigItem.SourceItem(
|
|
||||||
source = it,
|
|
||||||
summary = it.getLocaleTitle(),
|
|
||||||
isEnabled = it in enabledSet,
|
|
||||||
isDraggable = false,
|
|
||||||
isAvailable = !isNsfwDisabled || !it.isNsfw(),
|
|
||||||
)
|
|
||||||
}.ifEmpty {
|
|
||||||
listOf(SourceConfigItem.EmptySearchResult)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val map = allSources.groupByTo(TreeMap(LocaleKeyComparator())) {
|
|
||||||
if (it in enabledSet) {
|
|
||||||
KEY_ENABLED
|
|
||||||
} else {
|
|
||||||
it.locale
|
|
||||||
}
|
|
||||||
}
|
|
||||||
map.remove(KEY_ENABLED)
|
|
||||||
val result = ArrayList<SourceConfigItem>(allSources.size + map.size + 2)
|
|
||||||
if (enabledSources.isNotEmpty()) {
|
|
||||||
result += SourceConfigItem.Header(R.string.enabled_sources)
|
|
||||||
if (withTip) {
|
|
||||||
result += SourceConfigItem.Tip(TIP_REORDER, R.drawable.ic_tap_reorder, R.string.sources_reorder_tip)
|
|
||||||
}
|
|
||||||
enabledSources.mapTo(result) {
|
|
||||||
SourceConfigItem.SourceItem(
|
|
||||||
source = it,
|
|
||||||
summary = it.getLocaleTitle(),
|
|
||||||
isEnabled = true,
|
|
||||||
isDraggable = true,
|
|
||||||
isAvailable = false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (enabledSources.size != allSources.size) {
|
|
||||||
result += SourceConfigItem.Header(R.string.available_sources)
|
|
||||||
val comparator = compareBy<MangaSource, String>(AlphanumComparator()) { it.name }
|
|
||||||
for ((key, list) in map) {
|
|
||||||
list.sortWith(comparator)
|
|
||||||
val isExpanded = key in expanded
|
|
||||||
result += SourceConfigItem.LocaleGroup(
|
|
||||||
localeId = key,
|
|
||||||
title = getLocaleTitle(key),
|
|
||||||
isExpanded = isExpanded,
|
|
||||||
)
|
|
||||||
if (isExpanded) {
|
|
||||||
list.mapTo(result) {
|
|
||||||
SourceConfigItem.SourceItem(
|
|
||||||
source = it,
|
|
||||||
summary = null,
|
|
||||||
isEnabled = false,
|
|
||||||
isDraggable = false,
|
|
||||||
isAvailable = !isNsfwDisabled || !it.isNsfw(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
repository.setPositions(newSourcesList)
|
||||||
return result
|
yield()
|
||||||
}
|
|
||||||
|
|
||||||
private fun getLocaleTitle(localeKey: String?): String? {
|
|
||||||
val locale = Locale(localeKey ?: return null)
|
|
||||||
return locale.getDisplayLanguage(locale).toTitleCase(locale)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun observeTip() = settings.observeAsFlow(AppSettings.KEY_TIPS_CLOSED) {
|
|
||||||
isTipEnabled(TIP_REORDER)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun MangaSource.isNsfw() = contentType == ContentType.HENTAI
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import android.text.style.ForegroundColorSpan
|
|||||||
import android.text.style.RelativeSizeSpan
|
import android.text.style.RelativeSizeSpan
|
||||||
import android.text.style.SuperscriptSpan
|
import android.text.style.SuperscriptSpan
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.appcompat.widget.PopupMenu
|
||||||
import androidx.core.text.buildSpannedString
|
import androidx.core.text.buildSpannedString
|
||||||
import androidx.core.text.inSpans
|
import androidx.core.text.inSpans
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
@@ -34,7 +35,13 @@ import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
|
|||||||
|
|
||||||
fun sourceConfigHeaderDelegate() =
|
fun sourceConfigHeaderDelegate() =
|
||||||
adapterDelegateViewBinding<SourceConfigItem.Header, SourceConfigItem, ItemFilterHeaderBinding>(
|
adapterDelegateViewBinding<SourceConfigItem.Header, SourceConfigItem, ItemFilterHeaderBinding>(
|
||||||
{ layoutInflater, parent -> ItemFilterHeaderBinding.inflate(layoutInflater, parent, false) },
|
{ layoutInflater, parent ->
|
||||||
|
ItemFilterHeaderBinding.inflate(
|
||||||
|
layoutInflater,
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
|
|
||||||
bind {
|
bind {
|
||||||
@@ -44,104 +51,121 @@ fun sourceConfigHeaderDelegate() =
|
|||||||
|
|
||||||
fun sourceConfigGroupDelegate(
|
fun sourceConfigGroupDelegate(
|
||||||
listener: SourceConfigListener,
|
listener: SourceConfigListener,
|
||||||
) = adapterDelegateViewBinding<SourceConfigItem.LocaleGroup, SourceConfigItem, ItemExpandableBinding>(
|
) =
|
||||||
{ layoutInflater, parent -> ItemExpandableBinding.inflate(layoutInflater, parent, false) },
|
adapterDelegateViewBinding<SourceConfigItem.LocaleGroup, SourceConfigItem, ItemExpandableBinding>(
|
||||||
) {
|
{ layoutInflater, parent -> ItemExpandableBinding.inflate(layoutInflater, parent, false) },
|
||||||
|
) {
|
||||||
|
|
||||||
binding.root.setOnClickListener {
|
binding.root.setOnClickListener {
|
||||||
listener.onHeaderClick(item)
|
listener.onHeaderClick(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
bind {
|
bind {
|
||||||
binding.root.text = item.title ?: getString(R.string.various_languages)
|
binding.root.text = item.title ?: getString(R.string.various_languages)
|
||||||
binding.root.isChecked = item.isExpanded
|
binding.root.isChecked = item.isExpanded
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun sourceConfigItemCheckableDelegate(
|
fun sourceConfigItemCheckableDelegate(
|
||||||
listener: SourceConfigListener,
|
listener: SourceConfigListener,
|
||||||
coil: ImageLoader,
|
coil: ImageLoader,
|
||||||
lifecycleOwner: LifecycleOwner,
|
lifecycleOwner: LifecycleOwner,
|
||||||
) = adapterDelegateViewBinding<SourceConfigItem.SourceItem, SourceConfigItem, ItemSourceConfigCheckableBinding>(
|
) =
|
||||||
{ layoutInflater, parent -> ItemSourceConfigCheckableBinding.inflate(layoutInflater, parent, false) },
|
adapterDelegateViewBinding<SourceConfigItem.SourceItem, SourceConfigItem, ItemSourceConfigCheckableBinding>(
|
||||||
) {
|
{ layoutInflater, parent ->
|
||||||
|
ItemSourceConfigCheckableBinding.inflate(
|
||||||
|
layoutInflater,
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
|
||||||
binding.switchToggle.setOnCheckedChangeListener { _, isChecked ->
|
binding.switchToggle.setOnCheckedChangeListener { _, isChecked ->
|
||||||
listener.onItemEnabledChanged(item, isChecked)
|
listener.onItemEnabledChanged(item, isChecked)
|
||||||
}
|
}
|
||||||
|
|
||||||
bind {
|
bind {
|
||||||
binding.textViewTitle.text = if (item.isNsfw) {
|
binding.textViewTitle.text = if (item.isNsfw) {
|
||||||
buildSpannedString {
|
buildSpannedString {
|
||||||
append(item.source.title)
|
append(item.source.title)
|
||||||
append(' ')
|
append(' ')
|
||||||
appendNsfwLabel(context)
|
appendNsfwLabel(context)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
item.source.title
|
||||||
|
}
|
||||||
|
binding.switchToggle.isChecked = item.isEnabled
|
||||||
|
binding.switchToggle.isEnabled = item.isAvailable
|
||||||
|
binding.textViewDescription.textAndVisible = item.summary
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
item.source.title
|
|
||||||
}
|
|
||||||
binding.switchToggle.isChecked = item.isEnabled
|
|
||||||
binding.switchToggle.isEnabled = item.isAvailable
|
|
||||||
binding.textViewDescription.textAndVisible = item.summary
|
|
||||||
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 sourceConfigItemDelegate2(
|
fun sourceConfigItemDelegate2(
|
||||||
listener: SourceConfigListener,
|
listener: SourceConfigListener,
|
||||||
coil: ImageLoader,
|
coil: ImageLoader,
|
||||||
lifecycleOwner: LifecycleOwner,
|
lifecycleOwner: LifecycleOwner,
|
||||||
) = adapterDelegateViewBinding<SourceConfigItem.SourceItem, SourceConfigItem, ItemSourceConfigBinding>(
|
) =
|
||||||
{ layoutInflater, parent -> ItemSourceConfigBinding.inflate(layoutInflater, parent, false) },
|
adapterDelegateViewBinding<SourceConfigItem.SourceItem, SourceConfigItem, ItemSourceConfigBinding>(
|
||||||
) {
|
{ layoutInflater, parent ->
|
||||||
|
ItemSourceConfigBinding.inflate(
|
||||||
|
layoutInflater,
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
|
||||||
val eventListener = View.OnClickListener { v ->
|
val eventListener = View.OnClickListener { v ->
|
||||||
when (v.id) {
|
when (v.id) {
|
||||||
R.id.imageView_add -> listener.onItemEnabledChanged(item, true)
|
R.id.imageView_add -> listener.onItemEnabledChanged(item, true)
|
||||||
R.id.imageView_remove -> listener.onItemEnabledChanged(item, false)
|
R.id.imageView_remove -> listener.onItemEnabledChanged(item, false)
|
||||||
R.id.imageView_config -> listener.onItemSettingsClick(item)
|
R.id.imageView_menu -> showSourceMenu(v, item, listener)
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.imageViewRemove.setOnClickListener(eventListener)
|
|
||||||
binding.imageViewAdd.setOnClickListener(eventListener)
|
|
||||||
binding.imageViewConfig.setOnClickListener(eventListener)
|
|
||||||
|
|
||||||
bind {
|
|
||||||
binding.textViewTitle.text = if (item.isNsfw) {
|
|
||||||
buildSpannedString {
|
|
||||||
append(item.source.title)
|
|
||||||
append(' ')
|
|
||||||
appendNsfwLabel(context)
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
item.source.title
|
|
||||||
}
|
}
|
||||||
binding.imageViewAdd.isGone = item.isEnabled || !item.isAvailable
|
binding.imageViewRemove.setOnClickListener(eventListener)
|
||||||
binding.imageViewRemove.isVisible = item.isEnabled
|
binding.imageViewAdd.setOnClickListener(eventListener)
|
||||||
binding.imageViewConfig.isVisible = item.isEnabled
|
binding.imageViewMenu.setOnClickListener(eventListener)
|
||||||
binding.textViewDescription.textAndVisible = item.summary
|
|
||||||
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
|
bind {
|
||||||
binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
|
binding.textViewTitle.text = if (item.isNsfw) {
|
||||||
crossfade(context)
|
buildSpannedString {
|
||||||
error(fallbackIcon)
|
append(item.source.title)
|
||||||
placeholder(fallbackIcon)
|
append(' ')
|
||||||
fallback(fallbackIcon)
|
appendNsfwLabel(context)
|
||||||
source(item.source)
|
}
|
||||||
enqueueWith(coil)
|
} else {
|
||||||
|
item.source.title
|
||||||
|
}
|
||||||
|
binding.imageViewAdd.isGone = item.isEnabled || !item.isAvailable
|
||||||
|
binding.imageViewRemove.isVisible = item.isEnabled
|
||||||
|
binding.imageViewMenu.isVisible = item.isEnabled
|
||||||
|
binding.textViewDescription.textAndVisible = item.summary
|
||||||
|
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 sourceConfigTipDelegate(
|
fun sourceConfigTipDelegate(
|
||||||
listener: OnTipCloseListener<SourceConfigItem.Tip>
|
listener: OnTipCloseListener<SourceConfigItem.Tip>,
|
||||||
) = adapterDelegateViewBinding<SourceConfigItem.Tip, SourceConfigItem, ItemTipBinding>(
|
) = adapterDelegateViewBinding<SourceConfigItem.Tip, SourceConfigItem, ItemTipBinding>(
|
||||||
{ layoutInflater, parent -> ItemTipBinding.inflate(layoutInflater, parent, false) },
|
{ layoutInflater, parent -> ItemTipBinding.inflate(layoutInflater, parent, false) },
|
||||||
) {
|
) {
|
||||||
@@ -156,14 +180,37 @@ fun sourceConfigTipDelegate(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sourceConfigEmptySearchDelegate() = adapterDelegate<SourceConfigItem.EmptySearchResult, SourceConfigItem>(
|
fun sourceConfigEmptySearchDelegate() =
|
||||||
R.layout.item_sources_empty,
|
adapterDelegate<SourceConfigItem.EmptySearchResult, SourceConfigItem>(
|
||||||
) { }
|
R.layout.item_sources_empty,
|
||||||
|
) { }
|
||||||
|
|
||||||
fun SpannableStringBuilder.appendNsfwLabel(context: Context) = inSpans(
|
fun SpannableStringBuilder.appendNsfwLabel(context: Context) = inSpans(
|
||||||
ForegroundColorSpan(context.getThemeColor(com.google.android.material.R.attr.colorError, Color.RED)),
|
ForegroundColorSpan(
|
||||||
|
context.getThemeColor(
|
||||||
|
com.google.android.material.R.attr.colorError,
|
||||||
|
Color.RED
|
||||||
|
)
|
||||||
|
),
|
||||||
RelativeSizeSpan(0.74f),
|
RelativeSizeSpan(0.74f),
|
||||||
SuperscriptSpan(),
|
SuperscriptSpan(),
|
||||||
) {
|
) {
|
||||||
append(context.getString(R.string.nsfw))
|
append(context.getString(R.string.nsfw))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showSourceMenu(
|
||||||
|
anchor: View,
|
||||||
|
item: SourceConfigItem.SourceItem,
|
||||||
|
listener: SourceConfigListener,
|
||||||
|
) {
|
||||||
|
val menu = PopupMenu(anchor.context, anchor)
|
||||||
|
menu.inflate(R.menu.popup_source_config)
|
||||||
|
menu.setOnMenuItemClickListener {
|
||||||
|
when (it.itemId) {
|
||||||
|
R.id.action_settings -> listener.onItemSettingsClick(item)
|
||||||
|
R.id.action_lift -> listener.onItemLiftClick(item)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
menu.show()
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ interface SourceConfigListener : OnTipCloseListener<SourceConfigItem.Tip> {
|
|||||||
|
|
||||||
fun onItemSettingsClick(item: SourceConfigItem.SourceItem)
|
fun onItemSettingsClick(item: SourceConfigItem.SourceItem)
|
||||||
|
|
||||||
|
fun onItemLiftClick(item: SourceConfigItem.SourceItem)
|
||||||
|
|
||||||
fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean)
|
fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean)
|
||||||
|
|
||||||
fun onHeaderClick(header: SourceConfigItem.LocaleGroup)
|
fun onHeaderClick(header: SourceConfigItem.LocaleGroup)
|
||||||
|
|||||||
@@ -72,11 +72,7 @@ sealed interface SourceConfigItem : ListModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object EmptySearchResult : SourceConfigItem {
|
data object EmptySearchResult : SourceConfigItem {
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
return other === EmptySearchResult
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||||
return other is EmptySearchResult
|
return other is EmptySearchResult
|
||||||
|
|||||||
@@ -52,14 +52,15 @@
|
|||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/imageView_config"
|
android:id="@+id/imageView_menu"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="?selectableItemBackgroundBorderless"
|
android:background="?selectableItemBackgroundBorderless"
|
||||||
android:contentDescription="@string/settings"
|
android:contentDescription="@string/more"
|
||||||
android:padding="@dimen/margin_small"
|
android:padding="@dimen/margin_small"
|
||||||
android:scaleType="center"
|
android:scaleType="center"
|
||||||
android:src="@drawable/ic_settings" />
|
android:src="@drawable/abc_ic_menu_overflow_material"
|
||||||
|
app:tint="?colorControlNormal" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/imageView_add"
|
android:id="@+id/imageView_add"
|
||||||
|
|||||||
@@ -16,11 +16,6 @@
|
|||||||
android:title="@string/disable_nsfw"
|
android:title="@string/disable_nsfw"
|
||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/action_locales"
|
|
||||||
android:title="@string/languages"
|
|
||||||
app:showAsAction="never" />
|
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_disable_all"
|
android:id="@+id/action_disable_all"
|
||||||
android:title="@string/disable_all"
|
android:title="@string/disable_all"
|
||||||
|
|||||||
13
app/src/main/res/menu/popup_source_config.xml
Normal file
13
app/src/main/res/menu/popup_source_config.xml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_lift"
|
||||||
|
android:title="@string/to_top" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_settings"
|
||||||
|
android:title="@string/settings" />
|
||||||
|
|
||||||
|
</menu>
|
||||||
@@ -483,4 +483,6 @@
|
|||||||
<string name="directories">Directories</string>
|
<string name="directories">Directories</string>
|
||||||
<string name="main_screen_sections">Main screen sections</string>
|
<string name="main_screen_sections">Main screen sections</string>
|
||||||
<string name="items_limit_exceeded">No more items can be added</string>
|
<string name="items_limit_exceeded">No more items can be added</string>
|
||||||
|
<string name="to_top">To top</string>
|
||||||
|
<string name="moved_to_top">Moved to top</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user