From 2793f6ce5292ba64fb4d1b92549c2d65226d6123 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 31 Jul 2023 12:08:33 +0300 Subject: [PATCH] Update sources manage screen --- .../browser/cloudflare/CaptchaNotifier.kt | 1 + .../kotatsu/core/ui/BaseListAdapter.kt | 6 +- .../kotatsu/settings/SettingsActivity.kt | 4 +- .../settings/onboard/OnboardDialogFragment.kt | 13 ++-- ...stFragment.kt => SourcesManageFragment.kt} | 14 ++-- ...ViewModel.kt => SourcesManageViewModel.kt} | 78 ++++++++++--------- app/src/main/res/menu/opt_sources.xml | 7 +- app/src/main/res/values/strings.xml | 1 + app/src/main/res/xml/pref_root.xml | 2 +- 9 files changed, 72 insertions(+), 54 deletions(-) rename app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/{SourcesListFragment.kt => SourcesManageFragment.kt} (95%) rename app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/{SourcesListViewModel.kt => SourcesManageViewModel.kt} (80%) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CaptchaNotifier.kt b/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CaptchaNotifier.kt index d57cc09a4..04a76345b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CaptchaNotifier.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CaptchaNotifier.kt @@ -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 diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseListAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseListAdapter.kt index 76cc7fc89..378e638b1 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseListAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseListAdapter.kt @@ -20,10 +20,10 @@ open class BaseListAdapter( .setBackgroundThreadExecutor(Dispatchers.Default.limitedParallelism(2).asExecutor()) .build(), *delegates, -), FlowCollector> { +), FlowCollector?> { - override suspend fun emit(value: List) = suspendCoroutine { cont -> - setItems(value, ContinuationResumeRunnable(cont)) + override suspend fun emit(value: List?) = suspendCoroutine { cont -> + setItems(value.orEmpty(), ContinuationResumeRunnable(cont)) } fun addDelegate(type: ListItemType, delegate: AdapterDelegate>): BaseListAdapter { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/SettingsActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/SettingsActivity.kt index cf695ed25..c73d73c9e 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/SettingsActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/SettingsActivity.kt @@ -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() diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/onboard/OnboardDialogFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/onboard/OnboardDialogFragment.kt index c88dcd3e5..931025253 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/onboard/OnboardDialogFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/onboard/OnboardDialogFragment.kt @@ -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) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourcesListFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourcesManageFragment.kt similarity index 95% rename from app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourcesListFragment.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourcesManageFragment.kt index 413bea9a2..861531338 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourcesListFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourcesManageFragment.kt @@ -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(), SourceConfigListener, RecyclerViewOwner { @@ -41,7 +42,7 @@ class SourcesListFragment : lateinit var coil: ImageLoader private var reorderHelper: ItemTouchHelper? = null - private val viewModel by viewModels() + private val viewModel by viewModels() 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 } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourcesListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourcesManageViewModel.kt similarity index 80% rename from app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourcesListViewModel.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourcesManageViewModel.kt index 73a1e3471..317f274e8 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourcesListViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourcesManageViewModel.kt @@ -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>(emptyList()) - val onActionDone = MutableEventFlow() + private val expandedGroups = MutableStateFlow(emptySet()) + private var searchQuery = MutableStateFlow(null) private val mutex = Mutex() - private val expandedGroups = HashSet() - 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() 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, + expanded: Set, + query: String?, + withTip: Boolean, + ): List { 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(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(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 { private val deviceLocales = LocaleListCompat.getAdjustedDefault() diff --git a/app/src/main/res/menu/opt_sources.xml b/app/src/main/res/menu/opt_sources.xml index 9ec1bc4f0..75a01436f 100644 --- a/app/src/main/res/menu/opt_sources.xml +++ b/app/src/main/res/menu/opt_sources.xml @@ -10,9 +10,14 @@ app:actionViewClass="androidx.appcompat.widget.SearchView" app:showAsAction="ifRoom|collapseActionView" /> + + - \ No newline at end of file + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 307eb633c..4a792b729 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -469,4 +469,5 @@ View list Show %s requires a captcha to be resolved to work properly + Languages diff --git a/app/src/main/res/xml/pref_root.xml b/app/src/main/res/xml/pref_root.xml index 6892b9035..6a8c04a09 100644 --- a/app/src/main/res/xml/pref_root.xml +++ b/app/src/main/res/xml/pref_root.xml @@ -9,7 +9,7 @@ android:title="@string/appearance" />