Sources catalog

This commit is contained in:
Koitharu
2023-11-12 11:32:55 +02:00
parent dd898579c9
commit b093a885c9
32 changed files with 828 additions and 309 deletions

View File

@@ -221,6 +221,9 @@
</intent-filter>
</activity>
<activity
android:name="org.koitharu.kotatsu.settings.sources.catalog.SourcesCatalogActivity"
android:label="@string/sources_catalog" />
<service
android:name="androidx.work.impl.foreground.SystemForegroundService"

View File

@@ -18,6 +18,9 @@ abstract class MangaSourcesDao {
@Query("SELECT * FROM sources WHERE enabled = 1 ORDER BY sort_key")
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")
abstract fun observeEnabled(): Flow<List<MangaSourceEntity>>

View File

@@ -1,5 +1,8 @@
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.MangaSource
import org.koitharu.kotatsu.parsers.util.toTitleCase
@@ -18,3 +21,18 @@ fun MangaSource(name: String): MangaSource {
}
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)
}

View File

@@ -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)
}
}
}
}

View File

@@ -46,6 +46,10 @@ class MangaSourcesRepository @Inject constructor(
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 ->
dao.observeEnabled().map {
it.toSources(skipNsfw)

View File

@@ -7,6 +7,7 @@ import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
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.ui.image.FaviconDrawable
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
@@ -48,8 +49,8 @@ fun exploreButtonsAD(
icon.setColorSchemeColors(
context.getThemeColor(
materialR.attr.colorPrimary,
Color.DKGRAY
)
Color.DKGRAY,
),
)
binding.buttonRandom.icon = icon
icon.start()
@@ -98,7 +99,7 @@ fun exploreSourceListItemAD(
ItemExploreSourceListBinding.inflate(
layoutInflater,
parent,
false
false,
)
},
on = { item, _, _ -> item is MangaSourceItem && !item.isGrid },
@@ -112,6 +113,7 @@ fun exploreSourceListItemAD(
bind {
binding.textViewTitle.text = item.source.title
binding.textViewSubtitle.text = item.source.getSummary(context)
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
fallback(fallbackIcon)
@@ -132,7 +134,7 @@ fun exploreSourceGridItemAD(
ItemExploreSourceGridBinding.inflate(
layoutInflater,
parent,
false
false,
)
},
on = { item, _, _ -> item is MangaSourceItem && item.isGrid },

View File

@@ -5,6 +5,7 @@ import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
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.ui.image.FaviconDrawable
import org.koitharu.kotatsu.core.util.ext.enqueueWith
@@ -40,6 +41,7 @@ fun searchSuggestionSourceAD(
} else {
item.source.title
}
binding.textViewSubtitle.text = item.source.getSummary(context)
binding.switchLocal.isChecked = item.isEnabled
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
binding.imageViewCover.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {

View File

@@ -1,6 +1,5 @@
package org.koitharu.kotatsu.settings
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.net.Uri
@@ -9,7 +8,6 @@ import android.os.Bundle
import android.provider.Settings
import android.view.View
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.app.LocaleManagerCompat
import androidx.preference.ListPreference
import androidx.preference.Preference
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.ui.BasePreferenceFragment
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.map
import org.koitharu.kotatsu.core.util.ext.postDelayed
import org.koitharu.kotatsu.core.util.ext.setDefaultValueCompat
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.settings.utils.ActivityListPreference
import org.koitharu.kotatsu.settings.utils.SliderPreference
import java.util.Locale
import javax.inject.Inject
@AndroidEntryPoint
@@ -110,7 +107,7 @@ class AppearanceSettingsFragment :
private fun initLocalePicker(preference: ListPreference) {
val locales = preference.context.getLocalesConfig()
.toList()
.sortedWith(LocaleComparator(preference.context))
.sortedWith(LocaleComparator())
preference.entries = Array(locales.size + 1) { i ->
if (i == 0) {
getString(R.string.automatic)
@@ -134,25 +131,4 @@ class AppearanceSettingsFragment :
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)
}
}
}
}
}

View File

@@ -65,8 +65,6 @@ class NewSourcesDialogFragment :
viewModel.onItemEnabledChanged(item, isEnabled)
}
override fun onHeaderClick(header: SourceConfigItem.LocaleGroup) = Unit
override fun onCloseTip(tip: SourceConfigItem.Tip) = Unit
companion object {

View File

@@ -8,7 +8,6 @@ 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.explore.data.MangaSourcesRepository
@@ -35,7 +34,6 @@ class NewSourcesViewModel @Inject constructor(
SourceConfigItem.SourceItem(
source = source,
isEnabled = enabled,
summary = source.getLocaleTitle(),
isDraggable = false,
isAvailable = !skipNsfw || source.contentType != ContentType.HENTAI,
)

View File

@@ -1,6 +1,5 @@
package org.koitharu.kotatsu.settings.sources
import androidx.core.os.LocaleListCompat
import androidx.room.InvalidationTracker
import dagger.hilt.android.ViewModelLifecycle
import dagger.hilt.android.scopes.ViewModelScoped
@@ -15,19 +14,12 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R
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.prefs.AppSettings
import org.koitharu.kotatsu.core.util.AlphanumComparator
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.explore.data.MangaSourcesRepository
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.toTitleCase
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
import java.util.Locale
import java.util.TreeMap
import javax.inject.Inject
@ViewModelScoped
@@ -39,7 +31,6 @@ class SourcesListProducer @Inject constructor(
private val scope = lifecycle.lifecycleScope
private var query: String = ""
private val expanded = HashSet<String?>()
val list = MutableStateFlow(emptyList<SourceConfigItem>())
private var job = scope.launch(Dispatchers.Default) {
@@ -67,27 +58,18 @@ class SourcesListProducer @Inject constructor(
onInvalidated(emptySet())
}
fun expandCollapse(group: String?) {
if (!expanded.remove(group)) {
expanded.add(group)
}
onInvalidated(emptySet())
}
private suspend fun buildList(): List<SourceConfigItem> {
val allSources = repository.allMangaSources
val enabledSources = repository.getEnabledSources()
val isNsfwDisabled = settings.isNsfwContentDisabled
val withTip = settings.isTipEnabled(TIP_REORDER)
val enabledSet = enabledSources.toEnumSet()
if (query.isNotEmpty()) {
return allSources.mapNotNull {
return enabledSources.mapNotNull {
if (!it.title.contains(query, ignoreCase = true)) {
return@mapNotNull null
}
SourceConfigItem.SourceItem(
source = it,
summary = it.getLocaleTitle(),
isEnabled = it in enabledSet,
isDraggable = false,
isAvailable = !isNsfwDisabled || !it.isNsfw(),
@@ -96,17 +78,8 @@ class SourcesListProducer @Inject constructor(
listOf(SourceConfigItem.EmptySearchResult)
}
}
val map = allSources.groupByTo(TreeMap(LocaleKeyComparator())) {
if (it in enabledSet) {
KEY_ENABLED
} else {
it.locale
}
}
map.remove(KEY_ENABLED)
val result = ArrayList<SourceConfigItem>(allSources.size + map.size + 2)
val result = ArrayList<SourceConfigItem>(enabledSources.size + 1)
if (enabledSources.isNotEmpty()) {
result += SourceConfigItem.Header(R.string.enabled_sources)
if (withTip) {
result += SourceConfigItem.Tip(
TIP_REORDER,
@@ -117,70 +90,17 @@ class SourcesListProducer @Inject constructor(
enabledSources.mapTo(result) {
SourceConfigItem.SourceItem(
source = it,
summary = it.getLocaleTitle(),
isEnabled = true,
isDraggable = true,
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
}
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 {
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"
}
}

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.settings.sources
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
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.sources.adapter.SourceConfigAdapter
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 javax.inject.Inject
@@ -77,7 +79,7 @@ class SourcesManageFragment :
viewModel.content.observe(viewLifecycleOwner, sourcesAdapter)
viewModel.onActionDone.observeEvent(
viewLifecycleOwner,
ReversibleActionObserver(binding.recyclerView)
ReversibleActionObserver(binding.recyclerView),
)
addMenuProvider(SourcesMenuProvider())
}
@@ -119,10 +121,6 @@ class SourcesManageFragment :
viewModel.setEnabled(item.source, isEnabled)
}
override fun onHeaderClick(header: SourceConfigItem.LocaleGroup) {
viewModel.expandOrCollapse(header.localeId)
}
override fun onCloseTip(tip: SourceConfigItem.Tip) {
viewModel.onTipClosed(tip)
}
@@ -143,6 +141,11 @@ class SourcesManageFragment :
}
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 -> {
viewModel.disableAll()
true

View File

@@ -103,10 +103,6 @@ class SourcesManageViewModel @Inject constructor(
}
}
fun expandOrCollapse(headerId: String?) {
listProducer.expandCollapse(headerId)
}
fun performSearch(query: String?) {
listProducer.setQuery(query?.trim().orEmpty())
}

View File

@@ -13,8 +13,6 @@ class SourceConfigAdapter(
init {
with(delegatesManager) {
addDelegate(sourceConfigHeaderDelegate())
addDelegate(sourceConfigGroupDelegate(listener))
addDelegate(sourceConfigItemDelegate2(listener, coil, lifecycleOwner))
addDelegate(sourceConfigEmptySearchDelegate())
addDelegate(sourceConfigTipDelegate(listener))

View File

@@ -18,6 +18,7 @@ import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
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.ui.image.FaviconDrawable
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.newImageRequest
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.ItemSourceConfigCheckableBinding
import org.koitharu.kotatsu.databinding.ItemTipBinding
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(
listener: SourceConfigListener,
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
) =
adapterDelegateViewBinding<SourceConfigItem.SourceItem, SourceConfigItem, ItemSourceConfigCheckableBinding>(
{ layoutInflater, parent ->
ItemSourceConfigCheckableBinding.inflate(
layoutInflater,
parent,
false,
)
},
) {
) = adapterDelegateViewBinding<SourceConfigItem.SourceItem, SourceConfigItem, ItemSourceConfigCheckableBinding>(
{ layoutInflater, parent ->
ItemSourceConfigCheckableBinding.inflate(
layoutInflater,
parent,
false,
)
},
) {
binding.switchToggle.setOnCheckedChangeListener { _, isChecked ->
listener.onItemEnabledChanged(item, isChecked)
binding.switchToggle.setOnCheckedChangeListener { _, isChecked ->
listener.onItemEnabledChanged(item, isChecked)
}
bind {
binding.textViewTitle.text = if (item.isNsfw) {
buildSpannedString {
append(item.source.title)
append(' ')
appendNsfwLabel(context)
}
} else {
item.source.title
}
bind {
binding.textViewTitle.text = if (item.isNsfw) {
buildSpannedString {
append(item.source.title)
append(' ')
appendNsfwLabel(context)
}
} else {
item.source.title
}
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)
}
binding.switchToggle.isChecked = item.isEnabled
binding.switchToggle.isEnabled = item.isAvailable
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 sourceConfigItemDelegate2(
listener: SourceConfigListener,
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
) =
adapterDelegateViewBinding<SourceConfigItem.SourceItem, SourceConfigItem, ItemSourceConfigBinding>(
{ layoutInflater, parent ->
ItemSourceConfigBinding.inflate(
layoutInflater,
parent,
false,
)
},
) {
) = adapterDelegateViewBinding<SourceConfigItem.SourceItem, SourceConfigItem, ItemSourceConfigBinding>(
{ layoutInflater, parent ->
ItemSourceConfigBinding.inflate(
layoutInflater,
parent,
false,
)
},
) {
val eventListener = View.OnClickListener { v ->
when (v.id) {
R.id.imageView_add -> listener.onItemEnabledChanged(item, true)
R.id.imageView_remove -> listener.onItemEnabledChanged(item, false)
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)
}
val eventListener = View.OnClickListener { v ->
when (v.id) {
R.id.imageView_add -> listener.onItemEnabledChanged(item, true)
R.id.imageView_remove -> listener.onItemEnabledChanged(item, false)
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.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(
listener: OnTipCloseListener<SourceConfigItem.Tip>,

View File

@@ -12,6 +12,4 @@ interface SourceConfigListener : OnTipCloseListener<SourceConfigItem.Tip> {
fun onItemShortcutClick(item: SourceConfigItem.SourceItem)
fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean)
fun onHeaderClick(header: SourceConfigItem.LocaleGroup)
}

View File

@@ -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
}
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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
}
}

View File

@@ -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()
}
}

View File

@@ -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())
}
}

View File

@@ -2,45 +2,15 @@ package org.koitharu.kotatsu.settings.sources.model
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaSource
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(
val source: MangaSource,
val isEnabled: Boolean,
val summary: String?,
val isDraggable: Boolean,
val isAvailable: Boolean,
) : SourceConfigItem {
@@ -51,14 +21,6 @@ sealed interface SourceConfigItem : ListModel {
override fun areItemsTheSame(other: ListModel): Boolean {
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(

View 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>

View File

@@ -1,12 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
<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="@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
android:id="@+id/imageView_icon"
@@ -23,19 +26,32 @@
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
tools:src="@tools:sample/backgrounds/scenic" />
<TextView
android:id="@+id/textView_title"
android:layout_width="0dp"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
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" />
android:layout_marginEnd="@dimen/margin_small"
android:orientation="vertical">
</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>

View File

@@ -4,10 +4,12 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="?attr/listPreferredItemHeightSmall"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
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
android:id="@+id/imageView_cover"
@@ -18,17 +20,34 @@
app:shapeAppearance="?shapeAppearanceCornerSmall"
tools:src="@tools:sample/backgrounds/scenic" />
<TextView
android:id="@+id/textView_title"
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="@dimen/margin_small"
android:layout_weight="1"
android:ellipsize="end"
android:singleLine="true"
android:textAppearance="?attr/textAppearanceBodyMedium"
tools:text="@tools:sample/lorem[2]" />
android:orientation="vertical">
<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>
<View
android:layout_width="1dp"
@@ -42,4 +61,4 @@
android:layout_height="match_parent"
android:paddingHorizontal="?listPreferredItemPaddingEnd" />
</LinearLayout>
</LinearLayout>

View 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>

View File

@@ -7,6 +7,7 @@
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"
@@ -36,7 +37,7 @@
android:layout_height="wrap_content"
android:ellipsize="end"
android:singleLine="true"
android:textAppearance="?attr/textAppearanceBodyLarge"
android:textAppearance="?attr/textAppearanceTitleSmall"
tools:text="@tools:sample/lorem[15]" />
<TextView

View File

@@ -3,6 +3,12 @@
xmlns:android="http://schemas.android.com/apk/res/android"
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
android:id="@+id/action_search"
android:icon="?actionModeWebSearchDrawable"

View 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>

View File

@@ -512,4 +512,13 @@
<string name="last_successful_backup">Last successful backup: %s</string>
<string name="speed_value">x%.1f</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>