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)
.setDefaults(NotificationCompat.DEFAULT_SOUND)
.setSmallIcon(android.R.drawable.stat_notify_error)
.setAutoCancel(true)
.setVisibility(
if (exception.source?.contentType == ContentType.HENTAI) {
NotificationCompat.VISIBILITY_SECRET

View File

@@ -20,10 +20,10 @@ open class BaseListAdapter<T : ListModel>(
.setBackgroundThreadExecutor(Dispatchers.Default.limitedParallelism(2).asExecutor())
.build(),
*delegates,
), FlowCollector<List<T>> {
), FlowCollector<List<T>?> {
override suspend fun emit(value: List<T>) = suspendCoroutine { cont ->
setItems(value, ContinuationResumeRunnable(cont))
override suspend fun emit(value: List<T>?) = suspendCoroutine { cont ->
setItems(value.orEmpty(), ContinuationResumeRunnable(cont))
}
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.settings.about.AboutSettingsFragment
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.userdata.UserDataSettingsFragment
@@ -155,7 +155,7 @@ class SettingsActivity :
intent.getSerializableExtraCompat(EXTRA_SOURCE) as? MangaSource ?: MangaSource.LOCAL,
)
ACTION_MANAGE_SOURCES -> SourcesListFragment()
ACTION_MANAGE_SOURCES -> SourcesManageFragment()
Intent.ACTION_VIEW -> {
when (intent.data?.host) {
HOST_ABOUT -> AboutSettingsFragment()

View File

@@ -4,6 +4,7 @@ import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@@ -45,9 +46,7 @@ class OnboardDialogFragment :
if (isWelcome) {
builder.setTitle(R.string.welcome)
} else {
builder
.setTitle(R.string.remote_sources)
.setNegativeButton(android.R.string.cancel, this)
builder.setTitle(R.string.remote_sources)
}
return builder
}
@@ -56,10 +55,12 @@ class OnboardDialogFragment :
super.onViewBindingCreated(binding, savedInstanceState)
val adapter = SourceLocalesAdapter(this)
binding.recyclerView.adapter = adapter
binding.textViewTitle.setText(R.string.onboard_text)
viewModel.list.observe(viewLifecycleOwner) {
adapter.items = it.orEmpty()
if (isWelcome) {
binding.textViewTitle.setText(R.string.onboard_text)
} else {
binding.textViewTitle.isVisible = false
}
viewModel.list.observe(viewLifecycleOwner, adapter)
}
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.main.ui.owners.AppBarOwner
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.SourceConfigListener
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
import javax.inject.Inject
@AndroidEntryPoint
class SourcesListFragment :
class SourcesManageFragment :
BaseFragment<FragmentSettingsSourcesBinding>(),
SourceConfigListener,
RecyclerViewOwner {
@@ -41,7 +42,7 @@ class SourcesListFragment :
lateinit var coil: ImageLoader
private var reorderHelper: ItemTouchHelper? = null
private val viewModel by viewModels<SourcesListViewModel>()
private val viewModel by viewModels<SourcesManageViewModel>()
override val recyclerView: RecyclerView
get() = requireViewBinding().recyclerView
@@ -61,9 +62,7 @@ class SourcesListFragment :
it.attachToRecyclerView(this)
}
}
viewModel.items.observe(viewLifecycleOwner) {
sourcesAdapter.items = it
}
viewModel.content.observe(viewLifecycleOwner, sourcesAdapter)
viewModel.onActionDone.observeEvent(viewLifecycleOwner, ReversibleActionObserver(binding.recyclerView))
addMenuProvider(SourcesMenuProvider())
}
@@ -124,6 +123,11 @@ class SourcesListFragment :
true
}
R.id.action_locales -> {
OnboardDialogFragment.show(childFragmentManager)
true
}
else -> false
}

View File

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

View File

@@ -10,9 +10,14 @@
app:actionViewClass="androidx.appcompat.widget.SearchView"
app:showAsAction="ifRoom|collapseActionView" />
<item
android:id="@+id/action_locales"
android:title="@string/languages"
app:showAsAction="never" />
<item
android:id="@+id/action_disable_all"
android:title="@string/disable_all"
app:showAsAction="never" />
</menu>
</menu>

View File

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

View File

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