Update sources manage screen

This commit is contained in:
Koitharu
2023-07-31 12:08:33 +03:00
parent b107801188
commit 2793f6ce52
9 changed files with 72 additions and 54 deletions

View File

@@ -39,6 +39,7 @@ class CaptchaNotifier(
.setPriority(NotificationCompat.PRIORITY_DEFAULT) .setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setDefaults(NotificationCompat.DEFAULT_SOUND) .setDefaults(NotificationCompat.DEFAULT_SOUND)
.setSmallIcon(android.R.drawable.stat_notify_error) .setSmallIcon(android.R.drawable.stat_notify_error)
.setAutoCancel(true)
.setVisibility( .setVisibility(
if (exception.source?.contentType == ContentType.HENTAI) { if (exception.source?.contentType == ContentType.HENTAI) {
NotificationCompat.VISIBILITY_SECRET NotificationCompat.VISIBILITY_SECRET

View File

@@ -20,10 +20,10 @@ open class BaseListAdapter<T : ListModel>(
.setBackgroundThreadExecutor(Dispatchers.Default.limitedParallelism(2).asExecutor()) .setBackgroundThreadExecutor(Dispatchers.Default.limitedParallelism(2).asExecutor())
.build(), .build(),
*delegates, *delegates,
), FlowCollector<List<T>> { ), FlowCollector<List<T>?> {
override suspend fun emit(value: List<T>) = suspendCoroutine { cont -> override suspend fun emit(value: List<T>?) = suspendCoroutine { cont ->
setItems(value, ContinuationResumeRunnable(cont)) setItems(value.orEmpty(), ContinuationResumeRunnable(cont))
} }
fun addDelegate(type: ListItemType, delegate: AdapterDelegate<List<T>>): BaseListAdapter<T> { fun addDelegate(type: ListItemType, delegate: AdapterDelegate<List<T>>): BaseListAdapter<T> {

View File

@@ -30,7 +30,7 @@ import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.settings.about.AboutSettingsFragment import org.koitharu.kotatsu.settings.about.AboutSettingsFragment
import org.koitharu.kotatsu.settings.sources.SourceSettingsFragment import org.koitharu.kotatsu.settings.sources.SourceSettingsFragment
import org.koitharu.kotatsu.settings.sources.SourcesListFragment import org.koitharu.kotatsu.settings.sources.SourcesManageFragment
import org.koitharu.kotatsu.settings.tracker.TrackerSettingsFragment import org.koitharu.kotatsu.settings.tracker.TrackerSettingsFragment
import org.koitharu.kotatsu.settings.userdata.UserDataSettingsFragment import org.koitharu.kotatsu.settings.userdata.UserDataSettingsFragment
@@ -155,7 +155,7 @@ class SettingsActivity :
intent.getSerializableExtraCompat(EXTRA_SOURCE) as? MangaSource ?: MangaSource.LOCAL, intent.getSerializableExtraCompat(EXTRA_SOURCE) as? MangaSource ?: MangaSource.LOCAL,
) )
ACTION_MANAGE_SOURCES -> SourcesListFragment() ACTION_MANAGE_SOURCES -> SourcesManageFragment()
Intent.ACTION_VIEW -> { Intent.ACTION_VIEW -> {
when (intent.data?.host) { when (intent.data?.host) {
HOST_ABOUT -> AboutSettingsFragment() HOST_ABOUT -> AboutSettingsFragment()

View File

@@ -4,6 +4,7 @@ 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
@@ -45,9 +46,7 @@ class OnboardDialogFragment :
if (isWelcome) { if (isWelcome) {
builder.setTitle(R.string.welcome) builder.setTitle(R.string.welcome)
} else { } else {
builder builder.setTitle(R.string.remote_sources)
.setTitle(R.string.remote_sources)
.setNegativeButton(android.R.string.cancel, this)
} }
return builder return builder
} }
@@ -56,10 +55,12 @@ 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
binding.textViewTitle.setText(R.string.onboard_text) if (isWelcome) {
viewModel.list.observe(viewLifecycleOwner) { binding.textViewTitle.setText(R.string.onboard_text)
adapter.items = it.orEmpty() } else {
binding.textViewTitle.isVisible = false
} }
viewModel.list.observe(viewLifecycleOwner, adapter)
} }
override fun onItemCheckedChanged(item: SourceLocale, isChecked: Boolean) { override fun onItemCheckedChanged(item: SourceLocale, isChecked: Boolean) {

View File

@@ -26,13 +26,14 @@ 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
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class SourcesListFragment : class SourcesManageFragment :
BaseFragment<FragmentSettingsSourcesBinding>(), BaseFragment<FragmentSettingsSourcesBinding>(),
SourceConfigListener, SourceConfigListener,
RecyclerViewOwner { RecyclerViewOwner {
@@ -41,7 +42,7 @@ class SourcesListFragment :
lateinit var coil: ImageLoader lateinit var coil: ImageLoader
private var reorderHelper: ItemTouchHelper? = null private var reorderHelper: ItemTouchHelper? = null
private val viewModel by viewModels<SourcesListViewModel>() private val viewModel by viewModels<SourcesManageViewModel>()
override val recyclerView: RecyclerView override val recyclerView: RecyclerView
get() = requireViewBinding().recyclerView get() = requireViewBinding().recyclerView
@@ -61,9 +62,7 @@ class SourcesListFragment :
it.attachToRecyclerView(this) it.attachToRecyclerView(this)
} }
} }
viewModel.items.observe(viewLifecycleOwner) { viewModel.content.observe(viewLifecycleOwner, sourcesAdapter)
sourcesAdapter.items = it
}
viewModel.onActionDone.observeEvent(viewLifecycleOwner, ReversibleActionObserver(binding.recyclerView)) viewModel.onActionDone.observeEvent(viewLifecycleOwner, ReversibleActionObserver(binding.recyclerView))
addMenuProvider(SourcesMenuProvider()) addMenuProvider(SourcesMenuProvider())
} }
@@ -124,6 +123,11 @@ class SourcesListFragment :
true true
} }
R.id.action_locales -> {
OnboardDialogFragment.show(childFragmentManager)
true
}
else -> false else -> false
} }

View File

@@ -1,17 +1,24 @@
package org.koitharu.kotatsu.settings.sources package org.koitharu.kotatsu.settings.sources
import androidx.annotation.CheckResult
import androidx.core.os.LocaleListCompat import androidx.core.os.LocaleListCompat
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.getLocaleTitle import org.koitharu.kotatsu.core.model.getLocaleTitle
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.AlphanumComparator
@@ -23,7 +30,6 @@ import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
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 org.koitharu.kotatsu.settings.sources.model.SourceConfigItem import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
import java.util.EnumSet
import java.util.Locale import java.util.Locale
import java.util.TreeMap import java.util.TreeMap
import javax.inject.Inject import javax.inject.Inject
@@ -34,26 +40,28 @@ private const val KEY_ENABLED = "!"
private const val TIP_REORDER = "src_reorder" private const val TIP_REORDER = "src_reorder"
@HiltViewModel @HiltViewModel
class SourcesListViewModel @Inject constructor( class SourcesManageViewModel @Inject constructor(
private val settings: AppSettings, private val settings: AppSettings,
private val repository: MangaSourcesRepository, private val repository: MangaSourcesRepository,
) : BaseViewModel() { ) : BaseViewModel() {
val items = MutableStateFlow<List<SourceConfigItem>>(emptyList()) private val expandedGroups = MutableStateFlow(emptySet<String?>())
val onActionDone = MutableEventFlow<ReversibleAction>() private var searchQuery = MutableStateFlow<String?>(null)
private val mutex = Mutex() private val mutex = Mutex()
private val expandedGroups = HashSet<String?>() val content = combine(
private var searchQuery: String? = null repository.observeEnabledSources(),
expandedGroups,
searchQuery,
observeTip(),
) { sources, groups, query, tip ->
buildList(sources, groups, query, tip)
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
init { val onActionDone = MutableEventFlow<ReversibleAction>()
launchAtomicJob(Dispatchers.Default) {
buildList()
}
}
fun reorderSources(oldPos: Int, newPos: Int): Boolean { fun reorderSources(oldPos: Int, newPos: Int): Boolean {
val snapshot = items.value.toMutableList() val snapshot = content.value
val item = (snapshot[oldPos] as? SourceConfigItem.SourceItem) ?: return false val item = (snapshot[oldPos] as? SourceConfigItem.SourceItem) ?: return false
if ((snapshot[newPos] as? SourceConfigItem.SourceItem)?.isDraggable != true) return false if ((snapshot[newPos] as? SourceConfigItem.SourceItem)?.isDraggable != true) return false
launchAtomicJob(Dispatchers.Default) { launchAtomicJob(Dispatchers.Default) {
@@ -67,13 +75,12 @@ class SourcesListViewModel @Inject constructor(
} }
} }
repository.setPosition(item.source, targetPosition) repository.setPosition(item.source, targetPosition)
buildList()
} }
return true return true
} }
fun canReorder(oldPos: Int, newPos: Int): Boolean { fun canReorder(oldPos: Int, newPos: Int): Boolean {
val snapshot = items.value.toMutableList() val snapshot = content.value
if ((snapshot[oldPos] as? SourceConfigItem.SourceItem)?.isEnabled != true) return false if ((snapshot[oldPos] as? SourceConfigItem.SourceItem)?.isEnabled != true) return false
return (snapshot[newPos] as? SourceConfigItem.SourceItem)?.isEnabled == true return (snapshot[newPos] as? SourceConfigItem.SourceItem)?.isEnabled == true
} }
@@ -84,49 +91,45 @@ class SourcesListViewModel @Inject constructor(
if (!isEnabled) { if (!isEnabled) {
onActionDone.call(ReversibleAction(R.string.source_disabled, rollback)) onActionDone.call(ReversibleAction(R.string.source_disabled, rollback))
} }
buildList()
} }
} }
fun disableAll() { fun disableAll() {
launchAtomicJob(Dispatchers.Default) { launchAtomicJob(Dispatchers.Default) {
repository.disableAllSources() repository.disableAllSources()
buildList()
} }
} }
fun expandOrCollapse(headerId: String?) { fun expandOrCollapse(headerId: String?) {
launchAtomicJob { val expanded = expandedGroups.value
if (headerId in expandedGroups) { expandedGroups.value = if (headerId in expanded) {
expandedGroups.remove(headerId) expanded - headerId
} else { } else {
expandedGroups.add(headerId) expanded + headerId
}
buildList()
} }
} }
fun performSearch(query: String?) { fun performSearch(query: String?) {
launchAtomicJob { searchQuery.value = query?.trim()
searchQuery = query?.trim()
buildList()
}
} }
fun onTipClosed(item: SourceConfigItem.Tip) { fun onTipClosed(item: SourceConfigItem.Tip) {
launchAtomicJob(Dispatchers.Default) { launchAtomicJob(Dispatchers.Default) {
settings.closeTip(item.key) settings.closeTip(item.key)
buildList()
} }
} }
private suspend fun buildList() = withContext(Dispatchers.Default) { @CheckResult
private fun buildList(
enabledSources: List<MangaSource>,
expanded: Set<String?>,
query: String?,
withTip: Boolean,
): List<SourceConfigItem> {
val allSources = repository.allMangaSources val allSources = repository.allMangaSources
val enabledSources = repository.getEnabledSources()
val enabledSet = enabledSources.toEnumSet() val enabledSet = enabledSources.toEnumSet()
val query = searchQuery
if (!query.isNullOrEmpty()) { if (!query.isNullOrEmpty()) {
items.value = allSources.mapNotNull { return allSources.mapNotNull {
if (!it.title.contains(query, ignoreCase = true)) { if (!it.title.contains(query, ignoreCase = true)) {
return@mapNotNull null return@mapNotNull null
} }
@@ -139,7 +142,6 @@ class SourcesListViewModel @Inject constructor(
}.ifEmpty { }.ifEmpty {
listOf(SourceConfigItem.EmptySearchResult) listOf(SourceConfigItem.EmptySearchResult)
} }
return@withContext
} }
val map = allSources.groupByTo(TreeMap(LocaleKeyComparator())) { val map = allSources.groupByTo(TreeMap(LocaleKeyComparator())) {
if (it in enabledSet) { if (it in enabledSet) {
@@ -152,7 +154,7 @@ class SourcesListViewModel @Inject constructor(
val result = ArrayList<SourceConfigItem>(allSources.size + map.size + 2) val result = ArrayList<SourceConfigItem>(allSources.size + map.size + 2)
if (enabledSources.isNotEmpty()) { if (enabledSources.isNotEmpty()) {
result += SourceConfigItem.Header(R.string.enabled_sources) result += SourceConfigItem.Header(R.string.enabled_sources)
if (settings.isTipEnabled(TIP_REORDER)) { if (withTip) {
result += SourceConfigItem.Tip(TIP_REORDER, R.drawable.ic_tap_reorder, R.string.sources_reorder_tip) result += SourceConfigItem.Tip(TIP_REORDER, R.drawable.ic_tap_reorder, R.string.sources_reorder_tip)
} }
enabledSources.mapTo(result) { enabledSources.mapTo(result) {
@@ -169,7 +171,7 @@ class SourcesListViewModel @Inject constructor(
val comparator = compareBy<MangaSource, String>(AlphanumComparator()) { it.name } val comparator = compareBy<MangaSource, String>(AlphanumComparator()) { it.name }
for ((key, list) in map) { for ((key, list) in map) {
list.sortWith(comparator) list.sortWith(comparator)
val isExpanded = key in expandedGroups val isExpanded = key in expanded
result += SourceConfigItem.LocaleGroup( result += SourceConfigItem.LocaleGroup(
localeId = key, localeId = key,
title = getLocaleTitle(key), title = getLocaleTitle(key),
@@ -187,7 +189,7 @@ class SourcesListViewModel @Inject constructor(
} }
} }
} }
items.value = result return result
} }
private fun getLocaleTitle(localeKey: String?): String? { private fun getLocaleTitle(localeKey: String?): String? {
@@ -204,6 +206,10 @@ class SourcesListViewModel @Inject constructor(
} }
} }
private fun observeTip() = settings.observeAsFlow(AppSettings.KEY_TIPS_CLOSED) {
isTipEnabled(TIP_REORDER)
}
private class LocaleKeyComparator : Comparator<String?> { private class LocaleKeyComparator : Comparator<String?> {
private val deviceLocales = LocaleListCompat.getAdjustedDefault() private val deviceLocales = LocaleListCompat.getAdjustedDefault()

View File

@@ -10,9 +10,14 @@
app:actionViewClass="androidx.appcompat.widget.SearchView" app:actionViewClass="androidx.appcompat.widget.SearchView"
app:showAsAction="ifRoom|collapseActionView" /> app:showAsAction="ifRoom|collapseActionView" />
<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"
app:showAsAction="never" /> app:showAsAction="never" />
</menu> </menu>

View File

@@ -469,4 +469,5 @@
<string name="view_list">View list</string> <string name="view_list">View list</string>
<string name="show">Show</string> <string name="show">Show</string>
<string name="captcha_required_summary">%s requires a captcha to be resolved to work properly</string> <string name="captcha_required_summary">%s requires a captcha to be resolved to work properly</string>
<string name="languages">Languages</string>
</resources> </resources>

View File

@@ -9,7 +9,7 @@
android:title="@string/appearance" /> android:title="@string/appearance" />
<PreferenceScreen <PreferenceScreen
android:fragment="org.koitharu.kotatsu.settings.sources.SourcesListFragment" android:fragment="org.koitharu.kotatsu.settings.sources.SourcesManageFragment"
android:icon="@drawable/ic_manga_source" android:icon="@drawable/ic_manga_source"
android:key="remote_sources" android:key="remote_sources"
android:title="@string/remote_sources" /> android:title="@string/remote_sources" />