Sources catalog
This commit is contained in:
@@ -221,6 +221,9 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name="org.koitharu.kotatsu.settings.sources.catalog.SourcesCatalogActivity"
|
||||||
|
android:label="@string/sources_catalog" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ abstract class MangaSourcesDao {
|
|||||||
@Query("SELECT * FROM sources WHERE enabled = 1 ORDER BY sort_key")
|
@Query("SELECT * FROM sources WHERE enabled = 1 ORDER BY sort_key")
|
||||||
abstract suspend fun findAllEnabled(): List<MangaSourceEntity>
|
abstract suspend fun findAllEnabled(): List<MangaSourceEntity>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM sources WHERE enabled = 0 ORDER BY sort_key")
|
||||||
|
abstract suspend fun findAllDisabled(): List<MangaSourceEntity>
|
||||||
|
|
||||||
@Query("SELECT * FROM sources WHERE enabled = 1 ORDER BY sort_key")
|
@Query("SELECT * FROM sources WHERE enabled = 1 ORDER BY sort_key")
|
||||||
abstract fun observeEnabled(): Flow<List<MangaSourceEntity>>
|
abstract fun observeEnabled(): Flow<List<MangaSourceEntity>>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package org.koitharu.kotatsu.core.model
|
package org.koitharu.kotatsu.core.model
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||||
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
|
||||||
@@ -18,3 +21,18 @@ fun MangaSource(name: String): MangaSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun MangaSource.isNsfw() = contentType == ContentType.HENTAI
|
fun MangaSource.isNsfw() = contentType == ContentType.HENTAI
|
||||||
|
|
||||||
|
@get:StringRes
|
||||||
|
val ContentType.titleResId
|
||||||
|
get() = when (this) {
|
||||||
|
ContentType.MANGA -> R.string.content_type_manga
|
||||||
|
ContentType.HENTAI -> R.string.content_type_hentai
|
||||||
|
ContentType.COMICS -> R.string.content_type_comics
|
||||||
|
ContentType.OTHER -> R.string.content_type_other
|
||||||
|
}
|
||||||
|
|
||||||
|
fun MangaSource.getSummary(context: Context): String {
|
||||||
|
val type = context.getString(contentType.titleResId)
|
||||||
|
val locale = getLocaleTitle() ?: context.getString(R.string.various_languages)
|
||||||
|
return context.getString(R.string.source_summary_pattern, type, locale)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package org.koitharu.kotatsu.core.util
|
||||||
|
|
||||||
|
import androidx.core.os.LocaleListCompat
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.map
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class LocaleComparator : Comparator<Locale?> {
|
||||||
|
|
||||||
|
private val deviceLocales = LocaleListCompat.getAdjustedDefault()//LocaleManagerCompat.getSystemLocales(context)
|
||||||
|
.map { it.language }
|
||||||
|
.distinct()
|
||||||
|
|
||||||
|
override fun compare(a: Locale?, b: Locale?): Int {
|
||||||
|
return if (a === b) {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
val indexA = if (a == null) -1 else deviceLocales.indexOf(a.language)
|
||||||
|
val indexB = if (b == null) -1 else deviceLocales.indexOf(b.language)
|
||||||
|
if (indexA < 0 && indexB < 0) {
|
||||||
|
compareValues(a?.language, b?.language)
|
||||||
|
} else {
|
||||||
|
-2 - (indexA - indexB)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,6 +46,10 @@ class MangaSourcesRepository @Inject constructor(
|
|||||||
return dao.findAllEnabled().toSources(settings.isNsfwContentDisabled)
|
return dao.findAllEnabled().toSources(settings.isNsfwContentDisabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getDisabledSources(): List<MangaSource> {
|
||||||
|
return dao.findAllDisabled().toSources(settings.isNsfwContentDisabled)
|
||||||
|
}
|
||||||
|
|
||||||
fun observeEnabledSources(): Flow<List<MangaSource>> = observeIsNsfwDisabled().flatMapLatest { skipNsfw ->
|
fun observeEnabledSources(): Flow<List<MangaSource>> = observeIsNsfwDisabled().flatMapLatest { skipNsfw ->
|
||||||
dao.observeEnabled().map {
|
dao.observeEnabled().map {
|
||||||
it.toSources(skipNsfw)
|
it.toSources(skipNsfw)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import androidx.swiperefreshlayout.widget.CircularProgressDrawable
|
|||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.model.getSummary
|
||||||
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
|
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
|
||||||
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
|
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
|
||||||
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
|
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
|
||||||
@@ -48,8 +49,8 @@ fun exploreButtonsAD(
|
|||||||
icon.setColorSchemeColors(
|
icon.setColorSchemeColors(
|
||||||
context.getThemeColor(
|
context.getThemeColor(
|
||||||
materialR.attr.colorPrimary,
|
materialR.attr.colorPrimary,
|
||||||
Color.DKGRAY
|
Color.DKGRAY,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
binding.buttonRandom.icon = icon
|
binding.buttonRandom.icon = icon
|
||||||
icon.start()
|
icon.start()
|
||||||
@@ -98,7 +99,7 @@ fun exploreSourceListItemAD(
|
|||||||
ItemExploreSourceListBinding.inflate(
|
ItemExploreSourceListBinding.inflate(
|
||||||
layoutInflater,
|
layoutInflater,
|
||||||
parent,
|
parent,
|
||||||
false
|
false,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
on = { item, _, _ -> item is MangaSourceItem && !item.isGrid },
|
on = { item, _, _ -> item is MangaSourceItem && !item.isGrid },
|
||||||
@@ -112,6 +113,7 @@ fun exploreSourceListItemAD(
|
|||||||
|
|
||||||
bind {
|
bind {
|
||||||
binding.textViewTitle.text = item.source.title
|
binding.textViewTitle.text = item.source.title
|
||||||
|
binding.textViewSubtitle.text = item.source.getSummary(context)
|
||||||
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
|
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
|
||||||
binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
|
binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
|
||||||
fallback(fallbackIcon)
|
fallback(fallbackIcon)
|
||||||
@@ -132,7 +134,7 @@ fun exploreSourceGridItemAD(
|
|||||||
ItemExploreSourceGridBinding.inflate(
|
ItemExploreSourceGridBinding.inflate(
|
||||||
layoutInflater,
|
layoutInflater,
|
||||||
parent,
|
parent,
|
||||||
false
|
false,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
on = { item, _, _ -> item is MangaSourceItem && item.isGrid },
|
on = { item, _, _ -> item is MangaSourceItem && item.isGrid },
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import androidx.lifecycle.LifecycleOwner
|
|||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.model.getSummary
|
||||||
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
|
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
|
||||||
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
|
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
|
||||||
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||||
@@ -40,6 +41,7 @@ fun searchSuggestionSourceAD(
|
|||||||
} else {
|
} else {
|
||||||
item.source.title
|
item.source.title
|
||||||
}
|
}
|
||||||
|
binding.textViewSubtitle.text = item.source.getSummary(context)
|
||||||
binding.switchLocal.isChecked = item.isEnabled
|
binding.switchLocal.isChecked = item.isEnabled
|
||||||
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
|
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
|
||||||
binding.imageViewCover.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
|
binding.imageViewCover.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package org.koitharu.kotatsu.settings
|
package org.koitharu.kotatsu.settings
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
@@ -9,7 +8,6 @@ import android.os.Bundle
|
|||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.core.app.LocaleManagerCompat
|
|
||||||
import androidx.preference.ListPreference
|
import androidx.preference.ListPreference
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
@@ -18,8 +16,8 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
|
|||||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||||
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
|
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
|
||||||
import org.koitharu.kotatsu.core.ui.util.ActivityRecreationHandle
|
import org.koitharu.kotatsu.core.ui.util.ActivityRecreationHandle
|
||||||
|
import org.koitharu.kotatsu.core.util.LocaleComparator
|
||||||
import org.koitharu.kotatsu.core.util.ext.getLocalesConfig
|
import org.koitharu.kotatsu.core.util.ext.getLocalesConfig
|
||||||
import org.koitharu.kotatsu.core.util.ext.map
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.postDelayed
|
import org.koitharu.kotatsu.core.util.ext.postDelayed
|
||||||
import org.koitharu.kotatsu.core.util.ext.setDefaultValueCompat
|
import org.koitharu.kotatsu.core.util.ext.setDefaultValueCompat
|
||||||
import org.koitharu.kotatsu.core.util.ext.toList
|
import org.koitharu.kotatsu.core.util.ext.toList
|
||||||
@@ -27,7 +25,6 @@ import org.koitharu.kotatsu.parsers.util.names
|
|||||||
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
||||||
import org.koitharu.kotatsu.settings.utils.ActivityListPreference
|
import org.koitharu.kotatsu.settings.utils.ActivityListPreference
|
||||||
import org.koitharu.kotatsu.settings.utils.SliderPreference
|
import org.koitharu.kotatsu.settings.utils.SliderPreference
|
||||||
import java.util.Locale
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
@@ -110,7 +107,7 @@ class AppearanceSettingsFragment :
|
|||||||
private fun initLocalePicker(preference: ListPreference) {
|
private fun initLocalePicker(preference: ListPreference) {
|
||||||
val locales = preference.context.getLocalesConfig()
|
val locales = preference.context.getLocalesConfig()
|
||||||
.toList()
|
.toList()
|
||||||
.sortedWith(LocaleComparator(preference.context))
|
.sortedWith(LocaleComparator())
|
||||||
preference.entries = Array(locales.size + 1) { i ->
|
preference.entries = Array(locales.size + 1) { i ->
|
||||||
if (i == 0) {
|
if (i == 0) {
|
||||||
getString(R.string.automatic)
|
getString(R.string.automatic)
|
||||||
@@ -134,25 +131,4 @@ class AppearanceSettingsFragment :
|
|||||||
getString(it.title)
|
getString(it.title)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class LocaleComparator(context: Context) : Comparator<Locale> {
|
|
||||||
|
|
||||||
private val deviceLocales = LocaleManagerCompat.getSystemLocales(context)
|
|
||||||
.map { it.language }
|
|
||||||
.distinct()
|
|
||||||
|
|
||||||
override fun compare(a: Locale, b: Locale): Int {
|
|
||||||
return if (a === b) {
|
|
||||||
0
|
|
||||||
} else {
|
|
||||||
val indexA = deviceLocales.indexOf(a.language)
|
|
||||||
val indexB = deviceLocales.indexOf(b.language)
|
|
||||||
if (indexA == -1 && indexB == -1) {
|
|
||||||
compareValues(a.language, b.language)
|
|
||||||
} else {
|
|
||||||
-2 - (indexA - indexB)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,8 +65,6 @@ class NewSourcesDialogFragment :
|
|||||||
viewModel.onItemEnabledChanged(item, isEnabled)
|
viewModel.onItemEnabledChanged(item, isEnabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onHeaderClick(header: SourceConfigItem.LocaleGroup) = Unit
|
|
||||||
|
|
||||||
override fun onCloseTip(tip: SourceConfigItem.Tip) = Unit
|
override fun onCloseTip(tip: SourceConfigItem.Tip) = Unit
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import kotlinx.coroutines.flow.StateFlow
|
|||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
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.ui.BaseViewModel
|
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||||
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
||||||
@@ -35,7 +34,6 @@ class NewSourcesViewModel @Inject constructor(
|
|||||||
SourceConfigItem.SourceItem(
|
SourceConfigItem.SourceItem(
|
||||||
source = source,
|
source = source,
|
||||||
isEnabled = enabled,
|
isEnabled = enabled,
|
||||||
summary = source.getLocaleTitle(),
|
|
||||||
isDraggable = false,
|
isDraggable = false,
|
||||||
isAvailable = !skipNsfw || source.contentType != ContentType.HENTAI,
|
isAvailable = !skipNsfw || source.contentType != ContentType.HENTAI,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package org.koitharu.kotatsu.settings.sources
|
package org.koitharu.kotatsu.settings.sources
|
||||||
|
|
||||||
import androidx.core.os.LocaleListCompat
|
|
||||||
import androidx.room.InvalidationTracker
|
import androidx.room.InvalidationTracker
|
||||||
import dagger.hilt.android.ViewModelLifecycle
|
import dagger.hilt.android.ViewModelLifecycle
|
||||||
import dagger.hilt.android.scopes.ViewModelScoped
|
import dagger.hilt.android.scopes.ViewModelScoped
|
||||||
@@ -15,19 +14,12 @@ import kotlinx.coroutines.flow.update
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.db.TABLE_SOURCES
|
import org.koitharu.kotatsu.core.db.TABLE_SOURCES
|
||||||
import org.koitharu.kotatsu.core.model.getLocaleTitle
|
|
||||||
import org.koitharu.kotatsu.core.model.isNsfw
|
import org.koitharu.kotatsu.core.model.isNsfw
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.util.AlphanumComparator
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.lifecycleScope
|
import org.koitharu.kotatsu.core.util.ext.lifecycleScope
|
||||||
import org.koitharu.kotatsu.core.util.ext.map
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.toEnumSet
|
import org.koitharu.kotatsu.core.util.ext.toEnumSet
|
||||||
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
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 org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
|
||||||
import java.util.Locale
|
|
||||||
import java.util.TreeMap
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ViewModelScoped
|
@ViewModelScoped
|
||||||
@@ -39,7 +31,6 @@ class SourcesListProducer @Inject constructor(
|
|||||||
|
|
||||||
private val scope = lifecycle.lifecycleScope
|
private val scope = lifecycle.lifecycleScope
|
||||||
private var query: String = ""
|
private var query: String = ""
|
||||||
private val expanded = HashSet<String?>()
|
|
||||||
val list = MutableStateFlow(emptyList<SourceConfigItem>())
|
val list = MutableStateFlow(emptyList<SourceConfigItem>())
|
||||||
|
|
||||||
private var job = scope.launch(Dispatchers.Default) {
|
private var job = scope.launch(Dispatchers.Default) {
|
||||||
@@ -67,27 +58,18 @@ class SourcesListProducer @Inject constructor(
|
|||||||
onInvalidated(emptySet())
|
onInvalidated(emptySet())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun expandCollapse(group: String?) {
|
|
||||||
if (!expanded.remove(group)) {
|
|
||||||
expanded.add(group)
|
|
||||||
}
|
|
||||||
onInvalidated(emptySet())
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun buildList(): List<SourceConfigItem> {
|
private suspend fun buildList(): List<SourceConfigItem> {
|
||||||
val allSources = repository.allMangaSources
|
|
||||||
val enabledSources = repository.getEnabledSources()
|
val enabledSources = repository.getEnabledSources()
|
||||||
val isNsfwDisabled = settings.isNsfwContentDisabled
|
val isNsfwDisabled = settings.isNsfwContentDisabled
|
||||||
val withTip = settings.isTipEnabled(TIP_REORDER)
|
val withTip = settings.isTipEnabled(TIP_REORDER)
|
||||||
val enabledSet = enabledSources.toEnumSet()
|
val enabledSet = enabledSources.toEnumSet()
|
||||||
if (query.isNotEmpty()) {
|
if (query.isNotEmpty()) {
|
||||||
return allSources.mapNotNull {
|
return enabledSources.mapNotNull {
|
||||||
if (!it.title.contains(query, ignoreCase = true)) {
|
if (!it.title.contains(query, ignoreCase = true)) {
|
||||||
return@mapNotNull null
|
return@mapNotNull null
|
||||||
}
|
}
|
||||||
SourceConfigItem.SourceItem(
|
SourceConfigItem.SourceItem(
|
||||||
source = it,
|
source = it,
|
||||||
summary = it.getLocaleTitle(),
|
|
||||||
isEnabled = it in enabledSet,
|
isEnabled = it in enabledSet,
|
||||||
isDraggable = false,
|
isDraggable = false,
|
||||||
isAvailable = !isNsfwDisabled || !it.isNsfw(),
|
isAvailable = !isNsfwDisabled || !it.isNsfw(),
|
||||||
@@ -96,17 +78,8 @@ class SourcesListProducer @Inject constructor(
|
|||||||
listOf(SourceConfigItem.EmptySearchResult)
|
listOf(SourceConfigItem.EmptySearchResult)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val map = allSources.groupByTo(TreeMap(LocaleKeyComparator())) {
|
val result = ArrayList<SourceConfigItem>(enabledSources.size + 1)
|
||||||
if (it in enabledSet) {
|
|
||||||
KEY_ENABLED
|
|
||||||
} else {
|
|
||||||
it.locale
|
|
||||||
}
|
|
||||||
}
|
|
||||||
map.remove(KEY_ENABLED)
|
|
||||||
val result = ArrayList<SourceConfigItem>(allSources.size + map.size + 2)
|
|
||||||
if (enabledSources.isNotEmpty()) {
|
if (enabledSources.isNotEmpty()) {
|
||||||
result += SourceConfigItem.Header(R.string.enabled_sources)
|
|
||||||
if (withTip) {
|
if (withTip) {
|
||||||
result += SourceConfigItem.Tip(
|
result += SourceConfigItem.Tip(
|
||||||
TIP_REORDER,
|
TIP_REORDER,
|
||||||
@@ -117,70 +90,17 @@ class SourcesListProducer @Inject constructor(
|
|||||||
enabledSources.mapTo(result) {
|
enabledSources.mapTo(result) {
|
||||||
SourceConfigItem.SourceItem(
|
SourceConfigItem.SourceItem(
|
||||||
source = it,
|
source = it,
|
||||||
summary = it.getLocaleTitle(),
|
|
||||||
isEnabled = true,
|
isEnabled = true,
|
||||||
isDraggable = true,
|
isDraggable = true,
|
||||||
isAvailable = false,
|
isAvailable = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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.sortWith(comparator)
|
|
||||||
val isExpanded = key in expanded
|
|
||||||
result += SourceConfigItem.LocaleGroup(
|
|
||||||
localeId = key,
|
|
||||||
title = getLocaleTitle(key),
|
|
||||||
isExpanded = isExpanded,
|
|
||||||
)
|
|
||||||
if (isExpanded) {
|
|
||||||
list.mapTo(result) {
|
|
||||||
SourceConfigItem.SourceItem(
|
|
||||||
source = it,
|
|
||||||
summary = null,
|
|
||||||
isEnabled = false,
|
|
||||||
isDraggable = false,
|
|
||||||
isAvailable = !isNsfwDisabled || !it.isNsfw(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
private class LocaleKeyComparator : Comparator<String?> {
|
|
||||||
|
|
||||||
private val deviceLocales = LocaleListCompat.getAdjustedDefault()
|
|
||||||
.map { it.language }
|
|
||||||
|
|
||||||
override fun compare(a: String?, b: String?): Int {
|
|
||||||
when {
|
|
||||||
a == b -> return 0
|
|
||||||
a == null -> return 1
|
|
||||||
b == null -> return -1
|
|
||||||
}
|
|
||||||
val ai = deviceLocales.indexOf(a!!)
|
|
||||||
val bi = deviceLocales.indexOf(b!!)
|
|
||||||
return when {
|
|
||||||
ai < 0 && bi < 0 -> a.compareTo(b)
|
|
||||||
ai < 0 -> 1
|
|
||||||
bi < 0 -> -1
|
|
||||||
else -> ai.compareTo(bi)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private fun getLocaleTitle(localeKey: String?): String? {
|
|
||||||
val locale = Locale(localeKey ?: return null)
|
|
||||||
return locale.getDisplayLanguage(locale).toTitleCase(locale)
|
|
||||||
}
|
|
||||||
|
|
||||||
private const val KEY_ENABLED = "!"
|
|
||||||
const val TIP_REORDER = "src_reorder"
|
const val TIP_REORDER = "src_reorder"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.koitharu.kotatsu.settings.sources
|
package org.koitharu.kotatsu.settings.sources
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
@@ -32,6 +33,7 @@ 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.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.catalog.SourcesCatalogActivity
|
||||||
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
|
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -77,7 +79,7 @@ class SourcesManageFragment :
|
|||||||
viewModel.content.observe(viewLifecycleOwner, sourcesAdapter)
|
viewModel.content.observe(viewLifecycleOwner, sourcesAdapter)
|
||||||
viewModel.onActionDone.observeEvent(
|
viewModel.onActionDone.observeEvent(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner,
|
||||||
ReversibleActionObserver(binding.recyclerView)
|
ReversibleActionObserver(binding.recyclerView),
|
||||||
)
|
)
|
||||||
addMenuProvider(SourcesMenuProvider())
|
addMenuProvider(SourcesMenuProvider())
|
||||||
}
|
}
|
||||||
@@ -119,10 +121,6 @@ class SourcesManageFragment :
|
|||||||
viewModel.setEnabled(item.source, isEnabled)
|
viewModel.setEnabled(item.source, isEnabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onHeaderClick(header: SourceConfigItem.LocaleGroup) {
|
|
||||||
viewModel.expandOrCollapse(header.localeId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCloseTip(tip: SourceConfigItem.Tip) {
|
override fun onCloseTip(tip: SourceConfigItem.Tip) {
|
||||||
viewModel.onTipClosed(tip)
|
viewModel.onTipClosed(tip)
|
||||||
}
|
}
|
||||||
@@ -143,6 +141,11 @@ class SourcesManageFragment :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
|
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
|
||||||
|
R.id.action_catalog -> {
|
||||||
|
startActivity(Intent(context, SourcesCatalogActivity::class.java))
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
R.id.action_disable_all -> {
|
R.id.action_disable_all -> {
|
||||||
viewModel.disableAll()
|
viewModel.disableAll()
|
||||||
true
|
true
|
||||||
|
|||||||
@@ -103,10 +103,6 @@ class SourcesManageViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun expandOrCollapse(headerId: String?) {
|
|
||||||
listProducer.expandCollapse(headerId)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun performSearch(query: String?) {
|
fun performSearch(query: String?) {
|
||||||
listProducer.setQuery(query?.trim().orEmpty())
|
listProducer.setQuery(query?.trim().orEmpty())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ class SourceConfigAdapter(
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
with(delegatesManager) {
|
with(delegatesManager) {
|
||||||
addDelegate(sourceConfigHeaderDelegate())
|
|
||||||
addDelegate(sourceConfigGroupDelegate(listener))
|
|
||||||
addDelegate(sourceConfigItemDelegate2(listener, coil, lifecycleOwner))
|
addDelegate(sourceConfigItemDelegate2(listener, coil, lifecycleOwner))
|
||||||
addDelegate(sourceConfigEmptySearchDelegate())
|
addDelegate(sourceConfigEmptySearchDelegate())
|
||||||
addDelegate(sourceConfigTipDelegate(listener))
|
addDelegate(sourceConfigTipDelegate(listener))
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import coil.ImageLoader
|
|||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.model.getSummary
|
||||||
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
|
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
|
||||||
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
|
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
|
||||||
import org.koitharu.kotatsu.core.ui.list.OnTipCloseListener
|
import org.koitharu.kotatsu.core.ui.list.OnTipCloseListener
|
||||||
@@ -26,144 +27,104 @@ import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
|||||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||||
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
||||||
import org.koitharu.kotatsu.core.util.ext.source
|
import org.koitharu.kotatsu.core.util.ext.source
|
||||||
import org.koitharu.kotatsu.core.util.ext.textAndVisible
|
|
||||||
import org.koitharu.kotatsu.databinding.ItemExpandableBinding
|
|
||||||
import org.koitharu.kotatsu.databinding.ItemFilterHeaderBinding
|
|
||||||
import org.koitharu.kotatsu.databinding.ItemSourceConfigBinding
|
import org.koitharu.kotatsu.databinding.ItemSourceConfigBinding
|
||||||
import org.koitharu.kotatsu.databinding.ItemSourceConfigCheckableBinding
|
import org.koitharu.kotatsu.databinding.ItemSourceConfigCheckableBinding
|
||||||
import org.koitharu.kotatsu.databinding.ItemTipBinding
|
import org.koitharu.kotatsu.databinding.ItemTipBinding
|
||||||
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
|
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
|
||||||
|
|
||||||
fun sourceConfigHeaderDelegate() =
|
|
||||||
adapterDelegateViewBinding<SourceConfigItem.Header, SourceConfigItem, ItemFilterHeaderBinding>(
|
|
||||||
{ layoutInflater, parent ->
|
|
||||||
ItemFilterHeaderBinding.inflate(
|
|
||||||
layoutInflater,
|
|
||||||
parent,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
|
|
||||||
bind {
|
|
||||||
binding.textViewTitle.setText(item.titleResId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sourceConfigGroupDelegate(
|
|
||||||
listener: SourceConfigListener,
|
|
||||||
) =
|
|
||||||
adapterDelegateViewBinding<SourceConfigItem.LocaleGroup, SourceConfigItem, ItemExpandableBinding>(
|
|
||||||
{ layoutInflater, parent -> ItemExpandableBinding.inflate(layoutInflater, parent, false) },
|
|
||||||
) {
|
|
||||||
|
|
||||||
binding.root.setOnClickListener {
|
|
||||||
listener.onHeaderClick(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
bind {
|
|
||||||
binding.root.text = item.title ?: getString(R.string.various_languages)
|
|
||||||
binding.root.isChecked = item.isExpanded
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sourceConfigItemCheckableDelegate(
|
fun sourceConfigItemCheckableDelegate(
|
||||||
listener: SourceConfigListener,
|
listener: SourceConfigListener,
|
||||||
coil: ImageLoader,
|
coil: ImageLoader,
|
||||||
lifecycleOwner: LifecycleOwner,
|
lifecycleOwner: LifecycleOwner,
|
||||||
) =
|
) = adapterDelegateViewBinding<SourceConfigItem.SourceItem, SourceConfigItem, ItemSourceConfigCheckableBinding>(
|
||||||
adapterDelegateViewBinding<SourceConfigItem.SourceItem, SourceConfigItem, ItemSourceConfigCheckableBinding>(
|
{ layoutInflater, parent ->
|
||||||
{ layoutInflater, parent ->
|
ItemSourceConfigCheckableBinding.inflate(
|
||||||
ItemSourceConfigCheckableBinding.inflate(
|
layoutInflater,
|
||||||
layoutInflater,
|
parent,
|
||||||
parent,
|
false,
|
||||||
false,
|
)
|
||||||
)
|
},
|
||||||
},
|
) {
|
||||||
) {
|
|
||||||
|
|
||||||
binding.switchToggle.setOnCheckedChangeListener { _, isChecked ->
|
binding.switchToggle.setOnCheckedChangeListener { _, isChecked ->
|
||||||
listener.onItemEnabledChanged(item, isChecked)
|
listener.onItemEnabledChanged(item, isChecked)
|
||||||
|
}
|
||||||
|
|
||||||
|
bind {
|
||||||
|
binding.textViewTitle.text = if (item.isNsfw) {
|
||||||
|
buildSpannedString {
|
||||||
|
append(item.source.title)
|
||||||
|
append(' ')
|
||||||
|
appendNsfwLabel(context)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
item.source.title
|
||||||
}
|
}
|
||||||
|
binding.switchToggle.isChecked = item.isEnabled
|
||||||
bind {
|
binding.switchToggle.isEnabled = item.isAvailable
|
||||||
binding.textViewTitle.text = if (item.isNsfw) {
|
binding.textViewDescription.text = item.source.getSummary(context)
|
||||||
buildSpannedString {
|
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
|
||||||
append(item.source.title)
|
binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
|
||||||
append(' ')
|
crossfade(context)
|
||||||
appendNsfwLabel(context)
|
error(fallbackIcon)
|
||||||
}
|
placeholder(fallbackIcon)
|
||||||
} else {
|
fallback(fallbackIcon)
|
||||||
item.source.title
|
source(item.source)
|
||||||
}
|
enqueueWith(coil)
|
||||||
binding.switchToggle.isChecked = item.isEnabled
|
|
||||||
binding.switchToggle.isEnabled = item.isAvailable
|
|
||||||
binding.textViewDescription.textAndVisible = item.summary
|
|
||||||
val fallbackIcon =
|
|
||||||
FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
|
|
||||||
binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
|
|
||||||
crossfade(context)
|
|
||||||
error(fallbackIcon)
|
|
||||||
placeholder(fallbackIcon)
|
|
||||||
fallback(fallbackIcon)
|
|
||||||
source(item.source)
|
|
||||||
enqueueWith(coil)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun sourceConfigItemDelegate2(
|
fun sourceConfigItemDelegate2(
|
||||||
listener: SourceConfigListener,
|
listener: SourceConfigListener,
|
||||||
coil: ImageLoader,
|
coil: ImageLoader,
|
||||||
lifecycleOwner: LifecycleOwner,
|
lifecycleOwner: LifecycleOwner,
|
||||||
) =
|
) = adapterDelegateViewBinding<SourceConfigItem.SourceItem, SourceConfigItem, ItemSourceConfigBinding>(
|
||||||
adapterDelegateViewBinding<SourceConfigItem.SourceItem, SourceConfigItem, ItemSourceConfigBinding>(
|
{ layoutInflater, parent ->
|
||||||
{ layoutInflater, parent ->
|
ItemSourceConfigBinding.inflate(
|
||||||
ItemSourceConfigBinding.inflate(
|
layoutInflater,
|
||||||
layoutInflater,
|
parent,
|
||||||
parent,
|
false,
|
||||||
false,
|
)
|
||||||
)
|
},
|
||||||
},
|
) {
|
||||||
) {
|
|
||||||
|
|
||||||
val eventListener = View.OnClickListener { v ->
|
val eventListener = View.OnClickListener { v ->
|
||||||
when (v.id) {
|
when (v.id) {
|
||||||
R.id.imageView_add -> listener.onItemEnabledChanged(item, true)
|
R.id.imageView_add -> listener.onItemEnabledChanged(item, true)
|
||||||
R.id.imageView_remove -> listener.onItemEnabledChanged(item, false)
|
R.id.imageView_remove -> listener.onItemEnabledChanged(item, false)
|
||||||
R.id.imageView_menu -> showSourceMenu(v, item, listener)
|
R.id.imageView_menu -> showSourceMenu(v, item, listener)
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.imageViewRemove.setOnClickListener(eventListener)
|
|
||||||
binding.imageViewAdd.setOnClickListener(eventListener)
|
|
||||||
binding.imageViewMenu.setOnClickListener(eventListener)
|
|
||||||
|
|
||||||
bind {
|
|
||||||
binding.textViewTitle.text = if (item.isNsfw) {
|
|
||||||
buildSpannedString {
|
|
||||||
append(item.source.title)
|
|
||||||
append(' ')
|
|
||||||
appendNsfwLabel(context)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
item.source.title
|
|
||||||
}
|
|
||||||
binding.imageViewAdd.isGone = item.isEnabled || !item.isAvailable
|
|
||||||
binding.imageViewRemove.isVisible = item.isEnabled
|
|
||||||
binding.imageViewMenu.isVisible = item.isEnabled
|
|
||||||
binding.textViewDescription.textAndVisible = item.summary
|
|
||||||
val fallbackIcon =
|
|
||||||
FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
|
|
||||||
binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
|
|
||||||
crossfade(context)
|
|
||||||
error(fallbackIcon)
|
|
||||||
placeholder(fallbackIcon)
|
|
||||||
fallback(fallbackIcon)
|
|
||||||
source(item.source)
|
|
||||||
enqueueWith(coil)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
binding.imageViewRemove.setOnClickListener(eventListener)
|
||||||
|
binding.imageViewAdd.setOnClickListener(eventListener)
|
||||||
|
binding.imageViewMenu.setOnClickListener(eventListener)
|
||||||
|
|
||||||
|
bind {
|
||||||
|
binding.textViewTitle.text = if (item.isNsfw) {
|
||||||
|
buildSpannedString {
|
||||||
|
append(item.source.title)
|
||||||
|
append(' ')
|
||||||
|
appendNsfwLabel(context)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
item.source.title
|
||||||
|
}
|
||||||
|
binding.imageViewAdd.isGone = item.isEnabled || !item.isAvailable
|
||||||
|
binding.imageViewRemove.isVisible = item.isEnabled
|
||||||
|
binding.imageViewMenu.isVisible = item.isEnabled
|
||||||
|
binding.textViewDescription.text = item.source.getSummary(context)
|
||||||
|
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
|
||||||
|
binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
|
||||||
|
crossfade(context)
|
||||||
|
error(fallbackIcon)
|
||||||
|
placeholder(fallbackIcon)
|
||||||
|
fallback(fallbackIcon)
|
||||||
|
source(item.source)
|
||||||
|
enqueueWith(coil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun sourceConfigTipDelegate(
|
fun sourceConfigTipDelegate(
|
||||||
listener: OnTipCloseListener<SourceConfigItem.Tip>,
|
listener: OnTipCloseListener<SourceConfigItem.Tip>,
|
||||||
|
|||||||
@@ -12,6 +12,4 @@ interface SourceConfigListener : OnTipCloseListener<SourceConfigItem.Tip> {
|
|||||||
fun onItemShortcutClick(item: SourceConfigItem.SourceItem)
|
fun onItemShortcutClick(item: SourceConfigItem.SourceItem)
|
||||||
|
|
||||||
fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean)
|
fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean)
|
||||||
|
|
||||||
fun onHeaderClick(header: SourceConfigItem.LocaleGroup)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package org.koitharu.kotatsu.settings.sources.catalog
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
|
||||||
|
sealed interface SourceCatalogItem : ListModel {
|
||||||
|
|
||||||
|
data class Source(
|
||||||
|
val source: MangaSource
|
||||||
|
) : SourceCatalogItem {
|
||||||
|
|
||||||
|
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||||
|
return other is Source && other.source == source
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Hint(
|
||||||
|
@DrawableRes val icon: Int,
|
||||||
|
@StringRes val title: Int,
|
||||||
|
@StringRes val text: Int,
|
||||||
|
) : SourceCatalogItem {
|
||||||
|
|
||||||
|
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||||
|
return other is Hint && other.title == title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package org.koitharu.kotatsu.settings.sources.catalog
|
||||||
|
|
||||||
|
import androidx.core.text.buildSpannedString
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import coil.ImageLoader
|
||||||
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.model.isNsfw
|
||||||
|
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
|
||||||
|
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
|
||||||
|
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.crossfade
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.source
|
||||||
|
import org.koitharu.kotatsu.databinding.ItemEmptyHintBinding
|
||||||
|
import org.koitharu.kotatsu.databinding.ItemSourceCatalogBinding
|
||||||
|
import org.koitharu.kotatsu.settings.sources.adapter.appendNsfwLabel
|
||||||
|
|
||||||
|
fun sourceCatalogItemSourceAD(
|
||||||
|
coil: ImageLoader,
|
||||||
|
lifecycleOwner: LifecycleOwner,
|
||||||
|
listener: OnListItemClickListener<SourceCatalogItem.Source>
|
||||||
|
) = adapterDelegateViewBinding<SourceCatalogItem.Source, SourceCatalogItem, ItemSourceCatalogBinding>(
|
||||||
|
{ layoutInflater, parent ->
|
||||||
|
ItemSourceCatalogBinding.inflate(layoutInflater, parent, false)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
|
||||||
|
binding.imageViewAdd.setOnClickListener { v ->
|
||||||
|
listener.onItemClick(item, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
bind {
|
||||||
|
binding.textViewTitle.text = if (item.source.isNsfw()) {
|
||||||
|
buildSpannedString {
|
||||||
|
append(item.source.title)
|
||||||
|
append(' ')
|
||||||
|
appendNsfwLabel(context)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
item.source.title
|
||||||
|
}
|
||||||
|
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
|
||||||
|
binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
|
||||||
|
crossfade(context)
|
||||||
|
error(fallbackIcon)
|
||||||
|
placeholder(fallbackIcon)
|
||||||
|
fallback(fallbackIcon)
|
||||||
|
source(item.source)
|
||||||
|
enqueueWith(coil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sourceCatalogItemHintAD(
|
||||||
|
coil: ImageLoader,
|
||||||
|
lifecycleOwner: LifecycleOwner,
|
||||||
|
) = adapterDelegateViewBinding<SourceCatalogItem.Hint, SourceCatalogItem, ItemEmptyHintBinding>(
|
||||||
|
{ inflater, parent -> ItemEmptyHintBinding.inflate(inflater, parent, false) },
|
||||||
|
) {
|
||||||
|
|
||||||
|
binding.buttonRetry.isVisible = false
|
||||||
|
|
||||||
|
bind {
|
||||||
|
binding.icon.newImageRequest(lifecycleOwner, item.icon)?.enqueueWith(coil)
|
||||||
|
binding.textPrimary.setText(item.title)
|
||||||
|
binding.textSecondary.setTextAndVisible(item.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package org.koitharu.kotatsu.settings.sources.catalog
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.core.graphics.Insets
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
|
import coil.ImageLoader
|
||||||
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
|
import com.google.android.material.tabs.TabLayout
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.model.titleResId
|
||||||
|
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||||
|
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||||
|
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.firstVisibleItemPosition
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.observe
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||||
|
import org.koitharu.kotatsu.databinding.ActivitySourcesCatalogBinding
|
||||||
|
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
||||||
|
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||||
|
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
||||||
|
import java.util.Locale
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class SourcesCatalogActivity : BaseActivity<ActivitySourcesCatalogBinding>(),
|
||||||
|
TabLayout.OnTabSelectedListener,
|
||||||
|
OnListItemClickListener<SourceCatalogItem.Source>,
|
||||||
|
AppBarOwner {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var coil: ImageLoader
|
||||||
|
|
||||||
|
override val appBar: AppBarLayout
|
||||||
|
get() = viewBinding.appbar
|
||||||
|
|
||||||
|
private val viewModel by viewModels<SourcesCatalogViewModel>()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(ActivitySourcesCatalogBinding.inflate(layoutInflater))
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
initTabs()
|
||||||
|
val sourcesAdapter = SourcesCatalogAdapter(this, coil, this)
|
||||||
|
with(viewBinding.recyclerView) {
|
||||||
|
setHasFixedSize(true)
|
||||||
|
adapter = sourcesAdapter
|
||||||
|
}
|
||||||
|
viewModel.content.observe(this, sourcesAdapter)
|
||||||
|
viewModel.onActionDone.observeEvent(
|
||||||
|
this,
|
||||||
|
ReversibleActionObserver(viewBinding.recyclerView),
|
||||||
|
)
|
||||||
|
viewModel.locale.observe(this) {
|
||||||
|
supportActionBar?.subtitle = it.getLocaleDisplayName()
|
||||||
|
}
|
||||||
|
addMenuProvider(SourcesCatalogMenuProvider(this, viewModel))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onWindowInsetsChanged(insets: Insets) {
|
||||||
|
viewBinding.root.updatePadding(
|
||||||
|
left = insets.left,
|
||||||
|
right = insets.right,
|
||||||
|
)
|
||||||
|
viewBinding.recyclerView.updatePadding(
|
||||||
|
bottom = insets.bottom + viewBinding.recyclerView.paddingTop,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemClick(item: SourceCatalogItem.Source, view: View) {
|
||||||
|
viewModel.addSource(item.source)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTabSelected(tab: TabLayout.Tab) {
|
||||||
|
viewModel.setContentType(ContentType.entries[tab.position])
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTabUnselected(tab: TabLayout.Tab) = Unit
|
||||||
|
|
||||||
|
override fun onTabReselected(tab: TabLayout.Tab) {
|
||||||
|
viewBinding.recyclerView.firstVisibleItemPosition = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initTabs() {
|
||||||
|
val tabs = viewBinding.tabs
|
||||||
|
for (type in ContentType.entries) {
|
||||||
|
val tab = tabs.newTab()
|
||||||
|
tab.setText(type.titleResId)
|
||||||
|
tabs.addTab(tab)
|
||||||
|
}
|
||||||
|
tabs.addOnTabSelectedListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String?.getLocaleDisplayName(): String {
|
||||||
|
if (this == null) {
|
||||||
|
return getString(R.string.various_languages)
|
||||||
|
}
|
||||||
|
val lc = Locale(this)
|
||||||
|
return lc.getDisplayLanguage(lc).toTitleCase(lc)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package org.koitharu.kotatsu.settings.sources.catalog
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import coil.ImageLoader
|
||||||
|
import org.koitharu.kotatsu.core.ui.BaseListAdapter
|
||||||
|
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||||
|
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
|
||||||
|
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||||
|
|
||||||
|
class SourcesCatalogAdapter(
|
||||||
|
listener: OnListItemClickListener<SourceCatalogItem.Source>,
|
||||||
|
coil: ImageLoader,
|
||||||
|
lifecycleOwner: LifecycleOwner,
|
||||||
|
) : BaseListAdapter<SourceCatalogItem>(), FastScroller.SectionIndexer {
|
||||||
|
|
||||||
|
init {
|
||||||
|
addDelegate(ListItemType.CHAPTER, sourceCatalogItemSourceAD(coil, lifecycleOwner, listener))
|
||||||
|
addDelegate(ListItemType.HINT_EMPTY, sourceCatalogItemHintAD(coil, lifecycleOwner))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSectionText(context: Context, position: Int): CharSequence? {
|
||||||
|
return (items.getOrNull(position) as? SourceCatalogItem.Source)?.source?.title?.take(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package org.koitharu.kotatsu.settings.sources.catalog
|
||||||
|
|
||||||
|
import androidx.room.InvalidationTracker
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import dagger.hilt.android.ViewModelLifecycle
|
||||||
|
import dagger.hilt.android.lifecycle.RetainedLifecycle
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.cancelAndJoin
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
|
import org.koitharu.kotatsu.core.db.TABLE_SOURCES
|
||||||
|
import org.koitharu.kotatsu.core.db.removeObserverAsync
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.lifecycleScope
|
||||||
|
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
||||||
|
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||||
|
|
||||||
|
class SourcesCatalogListProducer @AssistedInject constructor(
|
||||||
|
@Assisted private val locale: String?,
|
||||||
|
@Assisted private val contentType: ContentType,
|
||||||
|
@Assisted lifecycle: ViewModelLifecycle,
|
||||||
|
private val repository: MangaSourcesRepository,
|
||||||
|
private val database: MangaDatabase,
|
||||||
|
) : InvalidationTracker.Observer(TABLE_SOURCES), RetainedLifecycle.OnClearedListener {
|
||||||
|
|
||||||
|
private val scope = lifecycle.lifecycleScope
|
||||||
|
private var query: String = ""
|
||||||
|
val list = MutableStateFlow(emptyList<SourceCatalogItem>())
|
||||||
|
|
||||||
|
private var job = scope.launch(Dispatchers.Default) {
|
||||||
|
list.value = buildList()
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
scope.launch(Dispatchers.Default) {
|
||||||
|
database.invalidationTracker.addObserver(this@SourcesCatalogListProducer)
|
||||||
|
}
|
||||||
|
lifecycle.addOnClearedListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
database.invalidationTracker.removeObserverAsync(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onInvalidated(tables: Set<String>) {
|
||||||
|
val prevJob = job
|
||||||
|
job = scope.launch(Dispatchers.Default) {
|
||||||
|
prevJob.cancelAndJoin()
|
||||||
|
list.update { buildList() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setQuery(value: String) {
|
||||||
|
this.query = value
|
||||||
|
onInvalidated(emptySet())
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun buildList(): List<SourceCatalogItem> {
|
||||||
|
val sources = repository.getDisabledSources().toMutableList()
|
||||||
|
sources.retainAll { it.contentType == contentType && it.locale == locale }
|
||||||
|
if (query.isNotEmpty()) {
|
||||||
|
sources.retainAll { it.title.contains(query, ignoreCase = true) }
|
||||||
|
}
|
||||||
|
return if (sources.isEmpty()) {
|
||||||
|
listOf(
|
||||||
|
if (query.isEmpty()) {
|
||||||
|
SourceCatalogItem.Hint(
|
||||||
|
icon = R.drawable.ic_empty_feed,
|
||||||
|
title = R.string.no_manga_sources,
|
||||||
|
text = R.string.no_manga_sources_catalog_text,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
SourceCatalogItem.Hint(
|
||||||
|
icon = R.drawable.ic_empty_feed,
|
||||||
|
title = R.string.nothing_found,
|
||||||
|
text = R.string.no_manga_sources_found,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
sources.sortBy { it.title }
|
||||||
|
sources.map {
|
||||||
|
SourceCatalogItem.Source(
|
||||||
|
source = it,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AssistedFactory
|
||||||
|
interface Factory {
|
||||||
|
|
||||||
|
fun create(
|
||||||
|
locale: String?,
|
||||||
|
contentType: ContentType,
|
||||||
|
lifecycle: ViewModelLifecycle,
|
||||||
|
): SourcesCatalogListProducer
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package org.koitharu.kotatsu.settings.sources.catalog
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuInflater
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import androidx.appcompat.widget.PopupMenu
|
||||||
|
import androidx.appcompat.widget.SearchView
|
||||||
|
import androidx.core.view.MenuProvider
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
||||||
|
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
||||||
|
|
||||||
|
class SourcesCatalogMenuProvider(
|
||||||
|
private val activity: Activity,
|
||||||
|
private val viewModel: SourcesCatalogViewModel,
|
||||||
|
) : MenuProvider,
|
||||||
|
MenuItem.OnActionExpandListener,
|
||||||
|
SearchView.OnQueryTextListener {
|
||||||
|
|
||||||
|
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||||
|
menuInflater.inflate(R.menu.opt_sources_catalog, menu)
|
||||||
|
val searchMenuItem = menu.findItem(R.id.action_search)
|
||||||
|
searchMenuItem.setOnActionExpandListener(this)
|
||||||
|
val searchView = searchMenuItem.actionView as SearchView
|
||||||
|
searchView.setOnQueryTextListener(this)
|
||||||
|
searchView.setIconifiedByDefault(false)
|
||||||
|
searchView.queryHint = searchMenuItem.title
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
|
||||||
|
R.id.action_locales -> {
|
||||||
|
showLocalesMenu()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||||
|
(activity as? AppBarOwner)?.appBar?.setExpanded(false, true)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||||
|
(item.actionView as SearchView).setQuery("", false)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onQueryTextSubmit(query: String?): Boolean = false
|
||||||
|
|
||||||
|
override fun onQueryTextChange(newText: String?): Boolean {
|
||||||
|
viewModel.performSearch(newText.orEmpty())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showLocalesMenu() {
|
||||||
|
val locales = viewModel.locales
|
||||||
|
val anchor: View = (activity as AppBarOwner).appBar.let {
|
||||||
|
it.findViewById<View?>(R.id.toolbar) ?: it
|
||||||
|
}
|
||||||
|
val menu = PopupMenu(activity, anchor)
|
||||||
|
for ((i, lc) in locales.withIndex()) {
|
||||||
|
val title = lc?.getDisplayLanguage(lc)?.toTitleCase(lc) ?: activity.getString(R.string.various_languages)
|
||||||
|
menu.menu.add(Menu.NONE, Menu.NONE, i, title)
|
||||||
|
}
|
||||||
|
menu.setOnMenuItemClickListener {
|
||||||
|
viewModel.setLocale(locales.getOrNull(it.order)?.language)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
menu.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package org.koitharu.kotatsu.settings.sources.catalog
|
||||||
|
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import dagger.hilt.android.internal.lifecycle.RetainedLifecycleImpl
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||||
|
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
||||||
|
import org.koitharu.kotatsu.core.util.LocaleComparator
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.call
|
||||||
|
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
||||||
|
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||||
|
import java.util.Locale
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class SourcesCatalogViewModel @Inject constructor(
|
||||||
|
private val repository: MangaSourcesRepository,
|
||||||
|
private val listProducerFactory: SourcesCatalogListProducer.Factory,
|
||||||
|
) : BaseViewModel() {
|
||||||
|
|
||||||
|
private val lifecycle = RetainedLifecycleImpl()
|
||||||
|
val onActionDone = MutableEventFlow<ReversibleAction>()
|
||||||
|
val contentType = MutableStateFlow(ContentType.entries.first())
|
||||||
|
val locales = getLocalesImpl()
|
||||||
|
val locale = MutableStateFlow(locales.firstOrNull()?.language)
|
||||||
|
|
||||||
|
private val listProducer: StateFlow<SourcesCatalogListProducer?> = combine(
|
||||||
|
locale,
|
||||||
|
contentType,
|
||||||
|
) { lc, type ->
|
||||||
|
listProducerFactory.create(lc, type, lifecycle)
|
||||||
|
}.stateIn(viewModelScope, SharingStarted.Eagerly, null)
|
||||||
|
|
||||||
|
val content = listProducer.flatMapLatest {
|
||||||
|
it?.list ?: emptyFlow()
|
||||||
|
}.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
lifecycle.dispatchOnCleared()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun performSearch(query: String) {
|
||||||
|
listProducer.value?.setQuery(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setLocale(value: String?) {
|
||||||
|
locale.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setContentType(value: ContentType) {
|
||||||
|
contentType.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addSource(source: MangaSource) {
|
||||||
|
launchJob(Dispatchers.Default) {
|
||||||
|
val rollback = repository.setSourceEnabled(source, true)
|
||||||
|
onActionDone.call(ReversibleAction(R.string.source_enabled, rollback))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getLocalesImpl(): List<Locale?> {
|
||||||
|
return repository.allMangaSources
|
||||||
|
.mapToSet { it.locale?.let(::Locale) }
|
||||||
|
.sortedWith(LocaleComparator())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,45 +2,15 @@ package org.koitharu.kotatsu.settings.sources.model
|
|||||||
|
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
|
||||||
sealed interface SourceConfigItem : ListModel {
|
sealed interface SourceConfigItem : ListModel {
|
||||||
|
|
||||||
data class Header(
|
|
||||||
@StringRes val titleResId: Int,
|
|
||||||
) : SourceConfigItem {
|
|
||||||
|
|
||||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
|
||||||
return other is Header && other.titleResId == titleResId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class LocaleGroup(
|
|
||||||
val localeId: String?,
|
|
||||||
val title: String?,
|
|
||||||
val isExpanded: Boolean,
|
|
||||||
) : SourceConfigItem {
|
|
||||||
|
|
||||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
|
||||||
return other is LocaleGroup && other.localeId == localeId
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getChangePayload(previousState: ListModel): Any? {
|
|
||||||
return if (previousState is LocaleGroup && previousState.isExpanded != isExpanded) {
|
|
||||||
ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED
|
|
||||||
} else {
|
|
||||||
super.getChangePayload(previousState)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class SourceItem(
|
data class SourceItem(
|
||||||
val source: MangaSource,
|
val source: MangaSource,
|
||||||
val isEnabled: Boolean,
|
val isEnabled: Boolean,
|
||||||
val summary: String?,
|
|
||||||
val isDraggable: Boolean,
|
val isDraggable: Boolean,
|
||||||
val isAvailable: Boolean,
|
val isAvailable: Boolean,
|
||||||
) : SourceConfigItem {
|
) : SourceConfigItem {
|
||||||
@@ -51,14 +21,6 @@ sealed interface SourceConfigItem : ListModel {
|
|||||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||||
return other is SourceItem && other.source == source
|
return other is SourceItem && other.source == source
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getChangePayload(previousState: ListModel): Any? {
|
|
||||||
return if (previousState is SourceItem && previousState.isEnabled != isEnabled) {
|
|
||||||
ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED
|
|
||||||
} else {
|
|
||||||
super.getChangePayload(previousState)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Tip(
|
data class Tip(
|
||||||
|
|||||||
47
app/src/main/res/layout/activity_sources_catalog.xml
Normal file
47
app/src/main/res/layout/activity_sources_catalog.xml
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".settings.sources.catalog.SourcesCatalogActivity">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:id="@+id/appbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fitsSystemWindows="true">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize" />
|
||||||
|
|
||||||
|
<com.google.android.material.tabs.TabLayout
|
||||||
|
android:id="@+id/tabs"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:tabGravity="start"
|
||||||
|
app:tabMode="scrollable" />
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/layout_list"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
|
||||||
|
|
||||||
|
<org.koitharu.kotatsu.core.ui.list.fastscroll.FastScrollRecyclerView
|
||||||
|
android:id="@+id/recyclerView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:orientation="vertical"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
|
tools:listitem="@layout/item_source_config" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<LinearLayout
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="@drawable/list_selector"
|
android:background="@drawable/list_selector"
|
||||||
android:clipChildren="false">
|
android:baselineAligned="false"
|
||||||
|
android:clipChildren="false"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
<com.google.android.material.imageview.ShapeableImageView
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
android:id="@+id/imageView_icon"
|
android:id="@+id/imageView_icon"
|
||||||
@@ -23,19 +26,32 @@
|
|||||||
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
|
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
|
||||||
tools:src="@tools:sample/backgrounds/scenic" />
|
tools:src="@tools:sample/backgrounds/scenic" />
|
||||||
|
|
||||||
<TextView
|
<LinearLayout
|
||||||
android:id="@+id/textView_title"
|
android:layout_width="match_parent"
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
android:layout_marginEnd="8dp"
|
android:layout_marginEnd="@dimen/margin_small"
|
||||||
android:ellipsize="end"
|
android:orientation="vertical">
|
||||||
android:singleLine="true"
|
|
||||||
android:textAppearance="?attr/textAppearanceBodyLarge"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@id/imageView_icon"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toEndOf="@+id/imageView_icon"
|
|
||||||
app:layout_constraintTop_toTopOf="@+id/imageView_icon"
|
|
||||||
tools:text="@tools:sample/lorem" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
<TextView
|
||||||
|
android:id="@+id/textView_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textAppearance="?attr/textAppearanceTitleSmall"
|
||||||
|
tools:text="@tools:sample/lorem[2]" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView_subtitle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
|
tools:text="@tools:sample/lorem[2]" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|||||||
@@ -4,10 +4,12 @@
|
|||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="?attr/listPreferredItemHeightSmall"
|
android:layout_height="wrap_content"
|
||||||
android:background="?selectableItemBackground"
|
android:background="?selectableItemBackground"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:orientation="horizontal">
|
android:minHeight="?attr/listPreferredItemHeightSmall"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingVertical="@dimen/margin_small">
|
||||||
|
|
||||||
<com.google.android.material.imageview.ShapeableImageView
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
android:id="@+id/imageView_cover"
|
android:id="@+id/imageView_cover"
|
||||||
@@ -18,17 +20,34 @@
|
|||||||
app:shapeAppearance="?shapeAppearanceCornerSmall"
|
app:shapeAppearance="?shapeAppearanceCornerSmall"
|
||||||
tools:src="@tools:sample/backgrounds/scenic" />
|
tools:src="@tools:sample/backgrounds/scenic" />
|
||||||
|
|
||||||
<TextView
|
<LinearLayout
|
||||||
android:id="@+id/textView_title"
|
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
android:layout_marginEnd="@dimen/margin_small"
|
android:layout_marginEnd="@dimen/margin_small"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:ellipsize="end"
|
android:orientation="vertical">
|
||||||
android:singleLine="true"
|
|
||||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
<TextView
|
||||||
tools:text="@tools:sample/lorem[2]" />
|
android:id="@+id/textView_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textAppearance="?attr/textAppearanceTitleSmall"
|
||||||
|
tools:text="@tools:sample/lorem[2]" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView_subtitle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
|
tools:text="@tools:sample/lorem[2]" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<View
|
<View
|
||||||
android:layout_width="1dp"
|
android:layout_width="1dp"
|
||||||
@@ -42,4 +61,4 @@
|
|||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:paddingHorizontal="?listPreferredItemPaddingEnd" />
|
android:paddingHorizontal="?listPreferredItemPaddingEnd" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
49
app/src/main/res/layout/item_source_catalog.xml
Normal file
49
app/src/main/res/layout/item_source_catalog.xml
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?android:windowBackground"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:minHeight="?listPreferredItemHeightSmall"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingVertical="@dimen/margin_small"
|
||||||
|
android:paddingStart="?listPreferredItemPaddingStart"
|
||||||
|
android:paddingEnd="?listPreferredItemPaddingEnd">
|
||||||
|
|
||||||
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
|
android:id="@+id/imageView_icon"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:background="?colorControlHighlight"
|
||||||
|
android:labelFor="@id/textView_title"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
app:shapeAppearance="?shapeAppearanceCornerSmall"
|
||||||
|
tools:src="@tools:sample/avatars" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView_title"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="?android:listPreferredItemPaddingStart"
|
||||||
|
android:layout_marginEnd="?android:listPreferredItemPaddingEnd"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textAppearance="?attr/textAppearanceTitleSmall"
|
||||||
|
tools:text="@tools:sample/lorem[15]" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/imageView_add"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?selectableItemBackgroundBorderless"
|
||||||
|
android:contentDescription="@string/add"
|
||||||
|
android:padding="@dimen/margin_small"
|
||||||
|
android:scaleType="center"
|
||||||
|
android:src="@drawable/ic_add"
|
||||||
|
android:tooltipText="@string/add" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="?android:windowBackground"
|
android:background="?android:windowBackground"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
|
android:minHeight="?listPreferredItemHeightSmall"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:paddingVertical="@dimen/margin_small"
|
android:paddingVertical="@dimen/margin_small"
|
||||||
android:paddingStart="?listPreferredItemPaddingStart"
|
android:paddingStart="?listPreferredItemPaddingStart"
|
||||||
@@ -36,7 +37,7 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:textAppearance="?attr/textAppearanceBodyLarge"
|
android:textAppearance="?attr/textAppearanceTitleSmall"
|
||||||
tools:text="@tools:sample/lorem[15]" />
|
tools:text="@tools:sample/lorem[15]" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
|||||||
@@ -3,6 +3,12 @@
|
|||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_catalog"
|
||||||
|
android:icon="@drawable/ic_add"
|
||||||
|
android:title="@string/sources_catalog"
|
||||||
|
app:showAsAction="ifRoom" />
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_search"
|
android:id="@+id/action_search"
|
||||||
android:icon="?actionModeWebSearchDrawable"
|
android:icon="?actionModeWebSearchDrawable"
|
||||||
|
|||||||
19
app/src/main/res/menu/opt_sources_catalog.xml
Normal file
19
app/src/main/res/menu/opt_sources_catalog.xml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_locales"
|
||||||
|
android:icon="@drawable/ic_expand_more"
|
||||||
|
android:title="@string/languages"
|
||||||
|
app:showAsAction="ifRoom" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_search"
|
||||||
|
android:icon="?actionModeWebSearchDrawable"
|
||||||
|
android:title="@string/search"
|
||||||
|
app:actionViewClass="androidx.appcompat.widget.SearchView"
|
||||||
|
app:showAsAction="ifRoom|collapseActionView" />
|
||||||
|
|
||||||
|
</menu>
|
||||||
@@ -512,4 +512,13 @@
|
|||||||
<string name="last_successful_backup">Last successful backup: %s</string>
|
<string name="last_successful_backup">Last successful backup: %s</string>
|
||||||
<string name="speed_value">x%.1f</string>
|
<string name="speed_value">x%.1f</string>
|
||||||
<string name="lock_screen_rotation">Lock screen rotation</string>
|
<string name="lock_screen_rotation">Lock screen rotation</string>
|
||||||
|
<string name="content_type_manga">Manga</string>
|
||||||
|
<string name="content_type_hentai">Hentai</string>
|
||||||
|
<string name="content_type_comics">Comics</string>
|
||||||
|
<string name="content_type_other">Other</string>
|
||||||
|
<string name="source_summary_pattern">%1$s, %2$s</string>
|
||||||
|
<string name="sources_catalog">Sources catalog</string>
|
||||||
|
<string name="source_enabled">Source enabled</string>
|
||||||
|
<string name="no_manga_sources_catalog_text">No available sources in this section yet. Stay tuned</string>
|
||||||
|
<string name="no_manga_sources_found">No available manga sources found by your query</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user