Update sources manage screen
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user