Store manga sources in database #426

This commit is contained in:
Koitharu
2023-07-24 15:47:52 +03:00
parent 03b92c4898
commit 376cee1859
22 changed files with 478 additions and 311 deletions

View File

@@ -1,16 +1,21 @@
package org.koitharu.kotatsu.settings
import android.content.SharedPreferences
import android.os.Bundle
import android.view.View
import androidx.annotation.StringRes
import androidx.fragment.app.viewModels
import androidx.preference.Preference
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.util.ext.observe
class RootSettingsFragment : BasePreferenceFragment(0), SharedPreferences.OnSharedPreferenceChangeListener {
@AndroidEntryPoint
class RootSettingsFragment : BasePreferenceFragment(0) {
private val viewModel: RootSettingsViewModel by viewModels()
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_root)
@@ -22,23 +27,18 @@ class RootSettingsFragment : BasePreferenceFragment(0), SharedPreferences.OnShar
bindPreferenceSummary("tracker", R.string.track_sources, R.string.notifications_settings)
bindPreferenceSummary("services", R.string.suggestions, R.string.sync, R.string.tracking)
findPreference<Preference>("about")?.summary = getString(R.string.app_version, BuildConfig.VERSION_NAME)
bindRemoteSourcesSummary()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
settings.subscribe(this)
}
override fun onDestroyView() {
settings.unsubscribe(this)
super.onDestroyView()
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
when (key) {
AppSettings.KEY_SOURCES_HIDDEN -> {
bindRemoteSourcesSummary()
findPreference<Preference>(AppSettings.KEY_REMOTE_SOURCES)?.let { pref ->
val total = viewModel.totalSourcesCount
viewModel.enabledSourcesCount.observe(viewLifecycleOwner) {
pref.summary = if (it >= 0) {
getString(R.string.enabled_d_of_d, it, total)
} else {
resources.getQuantityString(R.plurals.items, total, total)
}
}
}
}
@@ -46,11 +46,4 @@ class RootSettingsFragment : BasePreferenceFragment(0), SharedPreferences.OnShar
private fun bindPreferenceSummary(key: String, @StringRes vararg items: Int) {
findPreference<Preference>(key)?.summary = items.joinToString { getString(it) }
}
private fun bindRemoteSourcesSummary() {
findPreference<Preference>(AppSettings.KEY_REMOTE_SOURCES)?.run {
val total = settings.remoteMangaSources.size
summary = getString(R.string.enabled_d_of_d, total - settings.hiddenSources.size, total)
}
}
}

View File

@@ -0,0 +1,24 @@
package org.koitharu.kotatsu.settings
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
import javax.inject.Inject
@HiltViewModel
class RootSettingsViewModel @Inject constructor(
sourcesRepository: MangaSourcesRepository,
) : BaseViewModel() {
val totalSourcesCount = sourcesRepository.allMangaSources.size
val enabledSourcesCount = sourcesRepository.observeEnabledSources()
.map { it.size }
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, -1)
}

View File

@@ -9,7 +9,6 @@ import androidx.fragment.app.viewModels
import coil.ImageLoader
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.filterNotNull
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.AlertDialogFragment
import org.koitharu.kotatsu.core.util.ext.observe
@@ -39,8 +38,7 @@ class NewSourcesDialogFragment :
binding.recyclerView.adapter = adapter
binding.textViewTitle.setText(R.string.new_sources_text)
viewModel.sources.filterNotNull()
.observe(viewLifecycleOwner) { adapter.items = it }
viewModel.content.observe(viewLifecycleOwner) { adapter.items = it }
}
override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder {
@@ -51,7 +49,6 @@ class NewSourcesDialogFragment :
}
override fun onClick(dialog: DialogInterface, which: Int) {
viewModel.apply()
dialog.dismiss()
}

View File

@@ -1,74 +1,43 @@
package org.koitharu.kotatsu.settings.newsources
import androidx.annotation.WorkerThread
import androidx.core.os.LocaleListCompat
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.core.model.getLocaleTitle
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.mapToSet
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
import org.koitharu.kotatsu.parsers.util.SuspendLazy
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
import javax.inject.Inject
@HiltViewModel
class NewSourcesViewModel @Inject constructor(
private val settings: AppSettings,
private val repository: MangaSourcesRepository,
) : BaseViewModel() {
private val initialList = settings.newSources
val sources = MutableStateFlow<List<SourceConfigItem>?>(null)
private var listUpdateJob: Job? = null
init {
listUpdateJob = launchJob(Dispatchers.Default) {
sources.value = buildList()
}
private val newSources = SuspendLazy {
repository.assimilateNewSources()
}
val content: StateFlow<List<SourceConfigItem>> = repository.observeAll()
.map { sources ->
val new = newSources.get()
sources.mapNotNull { (source, enabled) ->
if (source in new) {
SourceConfigItem.SourceItem(source, enabled, source.getLocaleTitle(), false)
} else {
null
}
}
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, emptyList())
fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean) {
val prevJob = listUpdateJob
listUpdateJob = launchJob(Dispatchers.Default) {
if (isEnabled) {
settings.hiddenSources -= item.source.name
} else {
settings.hiddenSources += item.source.name
}
prevJob?.cancelAndJoin()
val list = buildList()
ensureActive()
sources.value = list
}
}
fun apply() {
settings.markKnownSources(initialList)
}
@WorkerThread
private fun buildList(): List<SourceConfigItem.SourceItem> {
val locales = LocaleListCompat.getDefault().mapToSet { it.language }
val pendingHidden = HashSet<String>()
return initialList.map {
val locale = it.locale
val isEnabledByLocale = locale == null || locale in locales
if (!isEnabledByLocale) {
pendingHidden += it.name
}
SourceConfigItem.SourceItem(
source = it,
summary = it.getLocaleTitle(),
isEnabled = isEnabledByLocale,
isDraggable = false,
)
}.also {
if (pendingHidden.isNotEmpty()) {
settings.hiddenSources += pendingHidden
}
launchJob(Dispatchers.Default) {
repository.setSourceEnabled(item.source, isEnabled)
}
}
}

View File

@@ -66,9 +66,9 @@ class OnboardDialogFragment :
viewModel.setItemChecked(item.key, isChecked)
}
override fun onClick(dialog: DialogInterface?, which: Int) {
override fun onClick(dialog: DialogInterface, which: Int) {
when (which) {
DialogInterface.BUTTON_POSITIVE -> viewModel.apply()
DialogInterface.BUTTON_POSITIVE -> dialog.dismiss()
}
}

View File

@@ -1,15 +1,16 @@
package org.koitharu.kotatsu.settings.onboard
import androidx.annotation.WorkerThread
import androidx.core.os.LocaleListCompat
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.map
import org.koitharu.kotatsu.core.util.ext.mapToSet
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.parsers.util.toTitleCase
import org.koitharu.kotatsu.settings.onboard.model.SourceLocale
import java.util.Locale
@@ -17,31 +18,34 @@ import javax.inject.Inject
@HiltViewModel
class OnboardViewModel @Inject constructor(
private val settings: AppSettings,
private val repository: MangaSourcesRepository,
) : BaseViewModel() {
private val allSources = settings.remoteMangaSources
private val allSources = repository.allMangaSources
private val locales = allSources.groupBy { it.locale }
private val selectedLocales = locales.keys.toMutableSet()
private val selectedLocales = HashSet<String?>()
val list = MutableStateFlow<List<SourceLocale>?>(null)
private var updateJob: Job
init {
if (settings.isSourcesSelected) {
selectedLocales.removeAll(settings.hiddenSources.mapNotNullToSet { x -> MangaSource(x).locale })
} else {
val deviceLocales = LocaleListCompat.getDefault().mapToSet { x ->
x.language
updateJob = launchJob(Dispatchers.Default) {
if (repository.isSetupRequired()) {
val deviceLocales = LocaleListCompat.getDefault().mapToSet { x ->
x.language
}
selectedLocales.addAll(deviceLocales)
if (selectedLocales.isEmpty()) {
selectedLocales += "en"
}
selectedLocales += null
} else {
selectedLocales.addAll(
repository.getEnabledSources().mapNotNullToSet { x -> x.locale },
)
}
selectedLocales.retainAll(deviceLocales)
if (selectedLocales.isEmpty()) {
selectedLocales += "en"
}
selectedLocales += null
rebuildList()
repository.assimilateNewSources()
}
rebuildList()
}
fun setItemChecked(key: String?, isChecked: Boolean) {
@@ -51,17 +55,17 @@ class OnboardViewModel @Inject constructor(
selectedLocales.remove(key)
}
if (isModified) {
rebuildList()
val prevJob = updateJob
updateJob = launchJob(Dispatchers.Default) {
prevJob.join()
val sources = allSources.filter { x -> x.locale == key }
repository.setSourcesEnabled(sources, isChecked)
rebuildList()
}
}
}
fun apply() {
settings.hiddenSources = allSources.filterNot { x ->
x.locale in selectedLocales
}.mapToSet { x -> x.name }
settings.markKnownSources(settings.newSources)
}
@WorkerThread
private fun rebuildList() {
list.value = locales.map { (key, srcs) ->
val locale = if (key != null) {

View File

@@ -3,25 +3,26 @@ package org.koitharu.kotatsu.settings.sources
import androidx.core.os.LocaleListCompat
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.runInterruptible
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.ui.BaseViewModel
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.ui.util.ReversibleHandle
import org.koitharu.kotatsu.core.util.AlphanumComparator
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.map
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.parsers.util.move
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,6 +35,7 @@ private const val TIP_REORDER = "src_reorder"
@HiltViewModel
class SourcesListViewModel @Inject constructor(
private val settings: AppSettings,
private val repository: MangaSourcesRepository,
) : BaseViewModel() {
val items = MutableStateFlow<List<SourceConfigItem>>(emptyList())
@@ -51,13 +53,19 @@ class SourcesListViewModel @Inject constructor(
fun reorderSources(oldPos: Int, newPos: Int): Boolean {
val snapshot = items.value.toMutableList()
if ((snapshot[oldPos] as? SourceConfigItem.SourceItem)?.isEnabled != true) return false
if ((snapshot[newPos] as? SourceConfigItem.SourceItem)?.isEnabled != true) return false
val item = (snapshot[oldPos] as? SourceConfigItem.SourceItem) ?: return false
if ((snapshot[newPos] as? SourceConfigItem.SourceItem)?.isDraggable != true) return false
launchAtomicJob(Dispatchers.Default) {
snapshot.move(oldPos, newPos)
settings.sourcesOrder = snapshot.mapNotNull {
(it as? SourceConfigItem.SourceItem)?.source?.name
var targetPosition = 0
for ((i, x) in snapshot.withIndex()) {
if (i == newPos) {
break
}
if (x is SourceConfigItem.SourceItem) {
targetPosition++
}
}
repository.setPosition(item.source, targetPosition)
buildList()
}
return true
@@ -71,17 +79,8 @@ class SourcesListViewModel @Inject constructor(
fun setEnabled(source: MangaSource, isEnabled: Boolean) {
launchAtomicJob(Dispatchers.Default) {
settings.hiddenSources = if (isEnabled) {
settings.hiddenSources - source.name
} else {
settings.hiddenSources + source.name
}
if (isEnabled) {
settings.markKnownSources(setOf(source))
} else {
val rollback = ReversibleHandle {
setEnabled(source, true)
}
val rollback = repository.setSourceEnabled(source, isEnabled)
if (!isEnabled) {
onActionDone.call(ReversibleAction(R.string.source_disabled, rollback))
}
buildList()
@@ -90,9 +89,7 @@ class SourcesListViewModel @Inject constructor(
fun disableAll() {
launchAtomicJob(Dispatchers.Default) {
settings.hiddenSources = settings.getMangaSources(includeHidden = true).mapToSet {
it.name
}
repository.disableAllSources()
buildList()
}
}
@@ -122,36 +119,37 @@ class SourcesListViewModel @Inject constructor(
}
}
private suspend fun buildList() = runInterruptible(Dispatchers.Default) {
val sources = settings.getMangaSources(includeHidden = true)
val hiddenSources = settings.hiddenSources
private suspend fun buildList() = withContext(Dispatchers.Default) {
val allSources = repository.allMangaSources
val enabledSources = repository.getEnabledSources()
val enabledSet = EnumSet.copyOf(enabledSources)
val query = searchQuery
if (!query.isNullOrEmpty()) {
items.value = sources.mapNotNull {
items.value = allSources.mapNotNull {
if (!it.title.contains(query, ignoreCase = true)) {
return@mapNotNull null
}
SourceConfigItem.SourceItem(
source = it,
summary = it.getLocaleTitle(),
isEnabled = it.name !in hiddenSources,
isEnabled = it in enabledSet,
isDraggable = false,
)
}.ifEmpty {
listOf(SourceConfigItem.EmptySearchResult)
}
return@runInterruptible
return@withContext
}
val map = sources.groupByTo(TreeMap(LocaleKeyComparator())) {
if (it.name !in hiddenSources) {
val map = allSources.groupByTo(TreeMap(LocaleKeyComparator())) {
if (it in enabledSet) {
KEY_ENABLED
} else {
it.locale
}
}
val result = ArrayList<SourceConfigItem>(sources.size + map.size + 2)
val enabledSources = map.remove(KEY_ENABLED)
if (!enabledSources.isNullOrEmpty()) {
map.remove(KEY_ENABLED)
val result = ArrayList<SourceConfigItem>(allSources.size + map.size + 2)
if (enabledSources.isNotEmpty()) {
result += SourceConfigItem.Header(R.string.enabled_sources)
if (settings.isTipEnabled(TIP_REORDER)) {
result += SourceConfigItem.Tip(TIP_REORDER, R.drawable.ic_tap_reorder, R.string.sources_reorder_tip)
@@ -165,10 +163,11 @@ class SourcesListViewModel @Inject constructor(
)
}
}
if (enabledSources?.size != sources.size) {
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.sortBy { it.ordinal }
list.sortWith(comparator)
val isExpanded = key in expandedGroups
result += SourceConfigItem.LocaleGroup(
localeId = key,
@@ -195,12 +194,12 @@ class SourcesListViewModel @Inject constructor(
return locale.getDisplayLanguage(locale).toTitleCase(locale)
}
private inline fun launchAtomicJob(
private fun launchAtomicJob(
context: CoroutineContext = EmptyCoroutineContext,
crossinline block: suspend CoroutineScope.() -> Unit
) = launchJob(context) {
block: suspend CoroutineScope.() -> Unit
) = launchJob(start = CoroutineStart.ATOMIC) {
mutex.withLock {
block()
withContext(context, block)
}
}