Update sources catalog and repository

This commit is contained in:
Koitharu
2024-06-01 17:13:27 +03:00
parent 87beb9442f
commit 18169c2355
31 changed files with 214 additions and 512 deletions

View File

@@ -16,8 +16,8 @@ android {
applicationId 'org.koitharu.kotatsu'
minSdk = 21
targetSdk = 34
versionCode = 645
versionName = '7.1.2'
versionCode = 646
versionName = '7.1.3'
generatedDensities = []
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
ksp {
@@ -82,7 +82,7 @@ afterEvaluate {
}
dependencies {
//noinspection GradleDependency
implementation('com.github.KotatsuApp:kotatsu-parsers:26be293f24') {
implementation('com.github.KotatsuApp:kotatsu-parsers:77a733a062') {
exclude group: 'org.json', module: 'json'
}

View File

@@ -24,6 +24,9 @@ abstract class MangaSourcesDao {
@Query("SELECT source FROM sources WHERE enabled = 1")
abstract suspend fun findAllEnabledNames(): List<String>
@Query("SELECT * FROM sources WHERE added_in >= :version")
abstract suspend fun findAllFromVersion(version: Int): List<MangaSourceEntity>
@Query("SELECT * FROM sources ORDER BY sort_key")
abstract fun observeAll(): Flow<List<MangaSourceEntity>>

View File

@@ -290,8 +290,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getBoolean(KEY_SOURCES_GRID, true)
set(value) = prefs.edit { putBoolean(KEY_SOURCES_GRID, value) }
val isNewSourcesTipEnabled: Boolean
get() = prefs.getBoolean(KEY_SOURCES_NEW, true)
var sourcesVersion: Int
get() = prefs.getInt(KEY_SOURCES_VERSION, 0)
set(value) = prefs.edit { putInt(KEY_SOURCES_VERSION, value) }
val isPagesNumbersEnabled: Boolean
get() = prefs.getBoolean(KEY_PAGES_NUMBERS, false)
@@ -653,7 +654,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_APP_LOCALE = "app_locale"
const val KEY_LOGGING_ENABLED = "logging"
const val KEY_SOURCES_GRID = "sources_grid"
const val KEY_SOURCES_NEW = "sources_new"
const val KEY_UPDATES_UNSTABLE = "updates_unstable"
const val KEY_TIPS_CLOSED = "tips_closed"
const val KEY_SSL_BYPASS = "ssl_bypass"
@@ -689,6 +689,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_STATS_ENABLED = "stats_on"
const val KEY_FEED_HEADER = "feed_header"
const val KEY_SEARCH_SUGGESTION_TYPES = "search_suggest_types"
const val KEY_SOURCES_VERSION = "sources_version"
// keys for non-persistent preferences
const val KEY_APP_VERSION = "app_version"

View File

@@ -12,6 +12,8 @@ import com.google.android.material.chip.ChipGroup
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.castOrNull
import com.google.android.material.R as materialR
class ChipsView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
@@ -48,7 +50,7 @@ class ChipsView @JvmOverloads constructor(
if (isInEditMode) {
setChips(
List(5) {
ChipModel(0, "Chip $it", 0, isCheckable = false, isChecked = false)
ChipModel(title = "Chip $it")
},
)
}
@@ -99,6 +101,15 @@ class ChipsView @JvmOverloads constructor(
chip.isChipIconVisible = true
}
chip.isChecked = model.isChecked
chip.isCheckedIconVisible = chip.isCheckable && model.icon == 0
chip.isCloseIconVisible = if (onChipCloseClickListener != null || model.isDropdown) {
chip.setCloseIconResource(
if (model.isDropdown) R.drawable.ic_expand_more else materialR.drawable.ic_m3_chip_close,
)
true
} else {
false
}
chip.tag = model.data
}
@@ -106,12 +117,11 @@ class ChipsView @JvmOverloads constructor(
val chip = Chip(context)
val drawable = ChipDrawable.createFromAttributes(context, null, 0, chipStyle)
chip.setChipDrawable(drawable)
chip.isCheckedIconVisible = true
chip.isChipIconVisible = false
chip.isCloseIconVisible = onChipCloseClickListener != null
chip.setOnCloseIconClickListener(chipOnCloseListener)
chip.setEnsureMinTouchTargetSize(false)
chip.setOnClickListener(chipOnClickListener)
chip.isElegantTextHeight = false
addView(chip)
return chip
}
@@ -127,11 +137,12 @@ class ChipsView @JvmOverloads constructor(
}
data class ChipModel(
@ColorRes val tint: Int,
val title: CharSequence,
@DrawableRes val icon: Int,
val isCheckable: Boolean,
val isChecked: Boolean,
@DrawableRes val icon: Int = 0,
val isCheckable: Boolean = false,
@ColorRes val tint: Int = 0,
val isChecked: Boolean = false,
val isDropdown: Boolean = false,
val data: Any? = null,
)

View File

@@ -612,10 +612,7 @@ class DetailsActivity :
ChipsView.ChipModel(
title = tag.title,
tint = tagHighlighter.getTagTint(tag),
icon = 0,
data = tag,
isCheckable = false,
isChecked = false,
)
},
)

View File

@@ -6,21 +6,22 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.dao.MangaSourcesDao
import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.isNsfw
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.ui.util.ReversibleHandle
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mapToSet
import java.util.Collections
import java.util.EnumSet
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
@Reusable
@@ -29,6 +30,7 @@ class MangaSourcesRepository @Inject constructor(
private val settings: AppSettings,
) {
private val isNewSourcesAssimilated = AtomicBoolean(false)
private val dao: MangaSourcesDao
get() = db.getSourcesDao()
@@ -43,25 +45,58 @@ class MangaSourcesRepository @Inject constructor(
get() = Collections.unmodifiableSet(remoteSources)
suspend fun getEnabledSources(): List<MangaSource> {
assimilateNewSources()
val order = settings.sourcesSortOrder
return dao.findAllEnabled(order).toSources(settings.isNsfwContentDisabled, order)
}
suspend fun getDisabledSources(): Set<MangaSource> {
assimilateNewSources()
val result = EnumSet.copyOf(remoteSources)
val enabled = dao.findAllEnabledNames()
for (name in enabled) {
val source = MangaSource(name)
val source = name.toMangaSourceOrNull() ?: continue
result.remove(source)
}
if (settings.isNsfwContentDisabled) {
result.removeAll { it.isNsfw() }
}
return result
}
suspend fun getAvailableSources(
isDisabledOnly: Boolean,
isNewOnly: Boolean,
excludeBroken: Boolean,
types: Set<ContentType>,
query: String?,
sortOrder: SourcesSortOrder?,
): List<MangaSource> {
assimilateNewSources()
val entities = dao.findAll().toMutableList()
if (isDisabledOnly) {
entities.removeAll { it.isEnabled }
}
if (isNewOnly) {
entities.retainAll { it.addedIn == BuildConfig.VERSION_CODE }
}
val sources = entities.toSources(
skipNsfwSources = settings.isNsfwContentDisabled,
sortOrder = sortOrder,
)
if (excludeBroken) {
sources.removeAll { it.isBroken }
}
if (types.isNotEmpty()) {
sources.retainAll { it.contentType in types }
}
if (!query.isNullOrEmpty()) {
sources.retainAll {
it.title.contains(query, ignoreCase = true) || it.name.contains(query, ignoreCase = true)
}
}
return sources
}
fun observeIsEnabled(source: MangaSource): Flow<Boolean> {
return dao.observeIsEnabled(source.name)
return dao.observeIsEnabled(source.name).onStart { assimilateNewSources() }
}
fun observeEnabledSourcesCount(): Flow<Int> {
@@ -69,8 +104,10 @@ class MangaSourcesRepository @Inject constructor(
observeIsNsfwDisabled(),
dao.observeEnabled(SourcesSortOrder.MANUAL),
) { skipNsfw, sources ->
sources.count { !skipNsfw || !MangaSource(it.source).isNsfw() }
}.distinctUntilChanged()
sources.count {
it.source.toMangaSourceOrNull()?.let { s -> !skipNsfw || !s.isNsfw() } == true
}
}.distinctUntilChanged().onStart { assimilateNewSources() }
}
fun observeAvailableSourcesCount(): Flow<Int> {
@@ -82,7 +119,7 @@ class MangaSourcesRepository @Inject constructor(
allMangaSources.count { x ->
x.name !in enabled && (!skipNsfw || !x.isNsfw())
}
}.distinctUntilChanged()
}.distinctUntilChanged().onStart { assimilateNewSources() }
}
fun observeEnabledSources(): Flow<List<MangaSource>> = combine(
@@ -92,18 +129,18 @@ class MangaSourcesRepository @Inject constructor(
dao.observeEnabled(order).map {
it.toSources(skipNsfw, order)
}
}.flatMapLatest { it }
}.flatMapLatest { it }.onStart { assimilateNewSources() }
fun observeAll(): Flow<List<Pair<MangaSource, Boolean>>> = dao.observeAll().map { entities ->
val result = ArrayList<Pair<MangaSource, Boolean>>(entities.size)
for (entity in entities) {
val source = MangaSource(entity.source)
val source = entity.source.toMangaSourceOrNull() ?: continue
if (source in remoteSources) {
result.add(source to entity.isEnabled)
}
}
result
}
}.onStart { assimilateNewSources() }
suspend fun setSourcesEnabled(sources: Collection<MangaSource>, isEnabled: Boolean): ReversibleHandle {
setSourcesEnabledImpl(sources, isEnabled)
@@ -114,6 +151,7 @@ class MangaSourcesRepository @Inject constructor(
suspend fun setSourcesEnabledExclusive(sources: Set<MangaSource>) {
db.withTransaction {
assimilateNewSources()
for (s in remoteSources) {
dao.setEnabled(s.name, s in sources)
}
@@ -135,32 +173,34 @@ class MangaSourcesRepository @Inject constructor(
}
}
fun observeNewSources(): Flow<Set<MangaSource>> = observeIsNewSourcesEnabled().flatMapLatest {
if (it) {
combine(
dao.observeAll(),
observeIsNsfwDisabled(),
) { entities, skipNsfw ->
val result = EnumSet.copyOf(remoteSources)
for (e in entities) {
result.remove(MangaSource(e.source))
}
if (skipNsfw) {
result.removeAll { x -> x.isNsfw() }
}
result
}.distinctUntilChanged()
fun observeHasNewSources(): Flow<Boolean> = observeIsNsfwDisabled().map { skipNsfw ->
val sources = dao.findAllFromVersion(BuildConfig.VERSION_CODE).toSources(skipNsfw, null)
sources.isNotEmpty()
}.onStart { assimilateNewSources() }
fun observeHasNewSourcesForBadge(): Flow<Boolean> = combine(
settings.observeAsFlow(AppSettings.KEY_SOURCES_VERSION) { sourcesVersion },
observeIsNsfwDisabled(),
) { version, skipNsfw ->
if (version < BuildConfig.VERSION_CODE) {
val sources = dao.findAllFromVersion(version).toSources(skipNsfw, null)
sources.isNotEmpty()
} else {
assimilateNewSources()
flowOf(emptySet())
false
}
}.onStart { assimilateNewSources() }
fun clearNewSourcesBadge() {
settings.sourcesVersion = BuildConfig.VERSION_CODE
}
@Deprecated("")
suspend fun assimilateNewSources(): Set<MangaSource> {
private suspend fun assimilateNewSources(): Boolean {
if (isNewSourcesAssimilated.getAndSet(true)) {
return false
}
val new = getNewSources()
if (new.isEmpty()) {
return emptySet()
return false
}
var maxSortKey = dao.getMaxSortKey()
val entities = new.map { x ->
@@ -172,14 +212,11 @@ class MangaSourcesRepository @Inject constructor(
)
}
dao.insertIfAbsent(entities)
if (settings.isNsfwContentDisabled) {
new.removeAll { x -> x.isNsfw() }
}
return new
return true
}
suspend fun isSetupRequired(): Boolean {
return dao.findAll().isEmpty()
return settings.sourcesVersion == 0 && dao.findAllEnabledNames().isEmpty()
}
private suspend fun setSourcesEnabledImpl(sources: Collection<MangaSource>, isEnabled: Boolean) {
@@ -198,7 +235,7 @@ class MangaSourcesRepository @Inject constructor(
val entities = dao.findAll()
val result = EnumSet.copyOf(remoteSources)
for (e in entities) {
result.remove(MangaSource(e.source))
result.remove(e.source.toMangaSourceOrNull() ?: continue)
}
return result
}
@@ -206,10 +243,10 @@ class MangaSourcesRepository @Inject constructor(
private fun List<MangaSourceEntity>.toSources(
skipNsfwSources: Boolean,
sortOrder: SourcesSortOrder?,
): List<MangaSource> {
): MutableList<MangaSource> {
val result = ArrayList<MangaSource>(size)
for (entity in this) {
val source = MangaSource(entity.source)
val source = entity.source.toMangaSourceOrNull() ?: continue
if (skipNsfwSources && source.isNsfw()) {
continue
}
@@ -227,11 +264,9 @@ class MangaSourcesRepository @Inject constructor(
isNsfwContentDisabled
}
private fun observeIsNewSourcesEnabled() = settings.observeAsFlow(AppSettings.KEY_SOURCES_NEW) {
isNewSourcesTipEnabled
}
private fun observeSortOrder() = settings.observeAsFlow(AppSettings.KEY_SOURCES_ORDER) {
sourcesSortOrder
}
private fun String.toMangaSourceOrNull(): MangaSource? = MangaSource.entries.find { it.name == this }
}

View File

@@ -27,7 +27,6 @@ import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.util.RecyclerViewOwner
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
import org.koitharu.kotatsu.core.ui.util.SpanSizeResolver
import org.koitharu.kotatsu.core.ui.widgets.TipView
import org.koitharu.kotatsu.core.util.ext.addMenuProvider
import org.koitharu.kotatsu.core.util.ext.findAppCompatDelegate
import org.koitharu.kotatsu.core.util.ext.observe
@@ -40,13 +39,11 @@ import org.koitharu.kotatsu.explore.ui.adapter.ExploreListEventListener
import org.koitharu.kotatsu.explore.ui.model.MangaSourceItem
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.TipModel
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
import org.koitharu.kotatsu.search.ui.MangaListActivity
import org.koitharu.kotatsu.settings.SettingsActivity
import org.koitharu.kotatsu.settings.newsources.NewSourcesDialogFragment
import org.koitharu.kotatsu.settings.sources.catalog.SourcesCatalogActivity
import org.koitharu.kotatsu.suggestions.ui.SuggestionsActivity
import javax.inject.Inject
@@ -56,7 +53,7 @@ class ExploreFragment :
BaseFragment<FragmentExploreBinding>(),
RecyclerViewOwner,
ExploreListEventListener,
OnListItemClickListener<MangaSourceItem>, TipView.OnButtonClickListener, ListSelectionController.Callback2 {
OnListItemClickListener<MangaSourceItem>, ListSelectionController.Callback2 {
@Inject
lateinit var coil: ImageLoader
@@ -74,7 +71,7 @@ class ExploreFragment :
override fun onViewBindingCreated(binding: FragmentExploreBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
exploreAdapter = ExploreAdapter(coil, viewLifecycleOwner, this, this, this) { manga, view ->
exploreAdapter = ExploreAdapter(coil, viewLifecycleOwner, this, this) { manga, view ->
startActivity(DetailsActivity.newIntent(view.context, manga))
}
sourceSelectionController = ListSelectionController(
@@ -124,18 +121,6 @@ class ExploreFragment :
}
}
override fun onPrimaryButtonClick(tipView: TipView) {
when ((tipView.tag as? TipModel)?.key) {
ExploreViewModel.TIP_NEW_SOURCES -> NewSourcesDialogFragment.show(childFragmentManager)
}
}
override fun onSecondaryButtonClick(tipView: TipView) {
when ((tipView.tag as? TipModel)?.key) {
ExploreViewModel.TIP_NEW_SOURCES -> viewModel.discardNewSources()
}
}
override fun onClick(v: View) {
val intent = when (v.id) {
R.id.button_local -> MangaListActivity.newIntent(v.context, MangaSource.LOCAL)

View File

@@ -102,12 +102,6 @@ class ExploreViewModel @Inject constructor(
}
}
fun discardNewSources() {
launchJob(Dispatchers.Default) {
sourcesRepository.assimilateNewSources()
}
}
fun requestPinShortcut(source: MangaSource) {
launchLoadingJob(Dispatchers.Default) {
shortcutManager.requestPinShortcut(source)
@@ -124,7 +118,7 @@ class ExploreViewModel @Inject constructor(
getSuggestionFlow(),
isGrid,
isRandomLoading,
sourcesRepository.observeNewSources(),
sourcesRepository.observeHasNewSourcesForBadge(),
) { content, suggestions, grid, randomLoading, newSources ->
buildList(content, suggestions, grid, randomLoading, newSources)
}.withErrorHandling()
@@ -134,7 +128,7 @@ class ExploreViewModel @Inject constructor(
recommendation: List<Manga>,
isGrid: Boolean,
randomLoading: Boolean,
newSources: Set<MangaSource>,
hasNewSources: Boolean,
): List<ListModel> {
val result = ArrayList<ListModel>(sources.size + 3)
result += ExploreButtons(randomLoading)
@@ -146,7 +140,7 @@ class ExploreViewModel @Inject constructor(
result += ListHeader(
textRes = R.string.remote_sources,
buttonTextRes = R.string.catalog,
badge = if (newSources.isNotEmpty()) "" else null,
badge = if (hasNewSources) "" else null,
)
sources.mapTo(result) { MangaSourceItem(it, isGrid) }
} else {
@@ -191,6 +185,5 @@ class ExploreViewModel @Inject constructor(
private const val TIP_SUGGESTIONS = "suggestions"
private const val SUGGESTIONS_COUNT = 8
const val TIP_NEW_SOURCES = "new_sources"
}
}

View File

@@ -4,13 +4,11 @@ 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.widgets.TipView
import org.koitharu.kotatsu.explore.ui.model.MangaSourceItem
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
import org.koitharu.kotatsu.list.ui.adapter.emptyHintAD
import org.koitharu.kotatsu.list.ui.adapter.listHeaderAD
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
import org.koitharu.kotatsu.list.ui.adapter.tipAD
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga
@@ -18,7 +16,6 @@ class ExploreAdapter(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
listener: ExploreListEventListener,
tipClickListener: TipView.OnButtonClickListener,
clickListener: OnListItemClickListener<MangaSourceItem>,
mangaClickListener: OnListItemClickListener<Manga>,
) : BaseListAdapter<ListModel>() {
@@ -34,6 +31,5 @@ class ExploreAdapter(
addDelegate(ListItemType.EXPLORE_SOURCE_GRID, exploreSourceGridItemAD(coil, clickListener, lifecycleOwner))
addDelegate(ListItemType.HINT_EMPTY, emptyHintAD(coil, lifecycleOwner, listener))
addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
addDelegate(ListItemType.TIP, tipAD(tipClickListener))
}
}

View File

@@ -257,7 +257,7 @@ class FilterCoordinator @Inject constructor(
}
oldValue.copy(
tagsExclude = newTags,
tags = oldValue.tags - newTags
tags = oldValue.tags - newTags,
)
}
}
@@ -308,7 +308,7 @@ class FilterCoordinator @Inject constructor(
currentState.update { oldValue ->
oldValue.copy(
tags = tags,
tagsExclude = oldValue.tagsExclude - tags
tagsExclude = oldValue.tagsExclude - tags,
)
}
}
@@ -391,9 +391,7 @@ class FilterCoordinator @Inject constructor(
val result = LinkedList<ChipsView.ChipModel>()
for (tag in tags) {
val model = ChipsView.ChipModel(
tint = 0,
title = tag.title,
icon = 0,
isCheckable = true,
isChecked = selectedTags.remove(tag),
data = tag,
@@ -406,9 +404,7 @@ class FilterCoordinator @Inject constructor(
}
for (tag in selectedTags) {
val model = ChipsView.ChipModel(
tint = 0,
title = tag.title,
icon = 0,
isCheckable = true,
isChecked = true,
data = tag,

View File

@@ -61,10 +61,7 @@ class FilterHeaderFragment : BaseFragment<FragmentFilterHeaderBinding>(), ChipsV
}
private fun moreTagsChip() = ChipsView.ChipModel(
tint = 0,
title = getString(R.string.more),
icon = materialR.drawable.abc_ic_menu_overflow_material,
isCheckable = false,
isChecked = false,
)
}

View File

@@ -144,9 +144,7 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
val chips = ArrayList<ChipsView.ChipModel>(value.selectedItems.size + value.availableItems.size + 1)
value.selectedItems.mapTo(chips) { tag ->
ChipsView.ChipModel(
tint = 0,
title = tag.title,
icon = 0,
isCheckable = true,
isChecked = true,
data = tag,
@@ -155,9 +153,7 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
value.availableItems.mapNotNullTo(chips) { tag ->
if (tag !in value.selectedItems) {
ChipsView.ChipModel(
tint = 0,
title = tag.title,
icon = 0,
isCheckable = true,
isChecked = false,
data = tag,
@@ -168,12 +164,8 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
}
chips.add(
ChipsView.ChipModel(
tint = 0,
title = getString(R.string.more),
icon = materialR.drawable.abc_ic_menu_overflow_material,
isCheckable = false,
isChecked = false,
data = null,
),
)
b.chipsGenres.setChips(chips)
@@ -200,9 +192,7 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
value.availableItems.mapNotNullTo(chips) { tag ->
if (tag !in value.selectedItems) {
ChipsView.ChipModel(
tint = 0,
title = tag.title,
icon = 0,
isCheckable = true,
isChecked = false,
data = tag,
@@ -213,12 +203,8 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
}
chips.add(
ChipsView.ChipModel(
tint = 0,
title = getString(R.string.more),
icon = materialR.drawable.abc_ic_menu_overflow_material,
isCheckable = false,
isChecked = false,
data = null,
),
)
b.chipsGenresExclude.setChips(chips)
@@ -233,9 +219,7 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
}
val chips = value.availableItems.map { state ->
ChipsView.ChipModel(
tint = 0,
title = getString(state.titleResId),
icon = 0,
isCheckable = true,
isChecked = state in value.selectedItems,
data = state,
@@ -253,9 +237,7 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
}
val chips = value.availableItems.map { contentRating ->
ChipsView.ChipModel(
tint = 0,
title = getString(contentRating.titleResId),
icon = 0,
isCheckable = true,
isChecked = contentRating in value.selectedItems,
data = contentRating,

View File

@@ -37,9 +37,6 @@ suspend fun Manga.toListDetailedModel(
ChipsView.ChipModel(
tint = extraProvider?.getTagTint(it) ?: 0,
title = it.title,
icon = 0,
isCheckable = false,
isChecked = false,
data = it,
)
},

View File

@@ -85,10 +85,7 @@ class PreviewViewModel @Inject constructor(
ChipsView.ChipModel(
title = tag.title,
tint = extraProvider.getTagTint(tag),
icon = 0,
data = tag,
isCheckable = false,
isChecked = false,
)
}
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())

View File

@@ -91,9 +91,7 @@ class WelcomeSheet : BaseAdaptiveSheet<SheetWelcomeBinding>(), ChipsView.OnChipC
chips.setChips(
value.availableItems.map {
ChipsView.ChipModel(
tint = 0,
title = it?.getDisplayLanguage(it)?.toTitleCase(it) ?: getString(R.string.various_languages),
icon = 0,
isCheckable = true,
isChecked = it in value.selectedItems,
data = it,
@@ -107,9 +105,7 @@ class WelcomeSheet : BaseAdaptiveSheet<SheetWelcomeBinding>(), ChipsView.OnChipC
chips.setChips(
value.availableItems.map {
ChipsView.ChipModel(
tint = 0,
title = getString(it.titleResId),
icon = 0,
isCheckable = true,
isChecked = it in value.selectedItems,
data = it,

View File

@@ -61,7 +61,7 @@ class WelcomeViewModel @Inject constructor(
selectedItems = selectedLocales,
isLoading = false,
)
repository.assimilateNewSources()
repository.clearNewSourcesBadge()
commit()
}
}

View File

@@ -172,12 +172,8 @@ class SearchSuggestionViewModel @Inject constructor(
private fun mapTags(tags: List<MangaTag>): List<ChipsView.ChipModel> = tags.map { tag ->
ChipsView.ChipModel(
tint = 0,
title = tag.title,
icon = 0,
data = tag,
isCheckable = false,
isChecked = false,
)
}
}

View File

@@ -1,76 +0,0 @@
package org.koitharu.kotatsu.settings.newsources
import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels
import coil.ImageLoader
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.AlertDialogFragment
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.databinding.DialogOnboardBinding
import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigListener
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
import javax.inject.Inject
@AndroidEntryPoint
class NewSourcesDialogFragment :
AlertDialogFragment<DialogOnboardBinding>(),
SourceConfigListener,
DialogInterface.OnClickListener {
@Inject
lateinit var coil: ImageLoader
private val viewModel by viewModels<NewSourcesViewModel>()
override fun onCreateViewBinding(
inflater: LayoutInflater,
container: ViewGroup?,
): DialogOnboardBinding {
return DialogOnboardBinding.inflate(inflater, container, false)
}
override fun onViewBindingCreated(binding: DialogOnboardBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
val adapter = SourcesSelectAdapter(this, coil, viewLifecycleOwner)
binding.recyclerView.adapter = adapter
binding.textViewTitle.setText(R.string.new_sources_text)
viewModel.content.observe(viewLifecycleOwner) { adapter.items = it }
}
override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder {
return super.onBuildDialog(builder)
.setPositiveButton(R.string.done, this)
.setCancelable(true)
.setTitle(R.string.remote_sources)
}
override fun onClick(dialog: DialogInterface, which: Int) {
dialog.dismiss()
}
override fun onItemSettingsClick(item: SourceConfigItem.SourceItem) = Unit
override fun onItemLiftClick(item: SourceConfigItem.SourceItem) = Unit
override fun onItemShortcutClick(item: SourceConfigItem.SourceItem) = Unit
override fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean) {
viewModel.onItemEnabledChanged(item, isEnabled)
}
override fun onCloseTip(tip: SourceConfigItem.Tip) = Unit
companion object {
private const val TAG = "NewSources"
fun show(fm: FragmentManager) = NewSourcesDialogFragment().show(fm, TAG)
}
}

View File

@@ -1,52 +0,0 @@
package org.koitharu.kotatsu.settings.newsources
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.util.SuspendLazy
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
import javax.inject.Inject
@HiltViewModel
class NewSourcesViewModel @Inject constructor(
private val repository: MangaSourcesRepository,
private val settings: AppSettings,
) : BaseViewModel() {
private val newSources = SuspendLazy {
repository.assimilateNewSources()
}
val content: StateFlow<List<SourceConfigItem>> = repository.observeAll()
.map { sources ->
val new = newSources.get()
val skipNsfw = settings.isNsfwContentDisabled
sources.mapNotNull { (source, enabled) ->
if (source in new) {
SourceConfigItem.SourceItem(
source = source,
isEnabled = enabled,
isDraggable = false,
isAvailable = !skipNsfw || source.contentType != ContentType.HENTAI,
)
} else {
null
}
}
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, emptyList())
fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean) {
launchJob(Dispatchers.Default) {
repository.setSourcesEnabled(setOf(item.source), isEnabled)
}
}
}

View File

@@ -1,19 +0,0 @@
package org.koitharu.kotatsu.settings.newsources
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigListener
import org.koitharu.kotatsu.settings.sources.adapter.sourceConfigItemCheckableDelegate
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
class SourcesSelectAdapter(
listener: SourceConfigListener,
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
) : BaseListAdapter<SourceConfigItem>() {
init {
delegatesManager.addDelegate(sourceConfigItemCheckableDelegate(listener, coil, lifecycleOwner))
}
}

View File

@@ -9,7 +9,6 @@ sealed interface SourceCatalogItem : ListModel {
data class Source(
val source: MangaSource,
val showSummary: Boolean,
) : SourceCatalogItem {
override fun areItemsTheSame(other: ListModel): Boolean {

View File

@@ -34,17 +34,15 @@ fun sourceCatalogItemSourceAD(
) {
binding.imageViewAdd.setOnClickListener { v ->
listener.onItemLongClick(item, v)
}
binding.root.setOnClickListener { v ->
listener.onItemClick(item, v)
}
bind {
binding.textViewTitle.text = item.source.getTitle(context)
if (item.showSummary) {
binding.textViewDescription.text = item.source.getSummary(context)
binding.textViewDescription.isVisible = true
} else {
binding.textViewDescription.isVisible = false
}
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)

View File

@@ -1,22 +1,27 @@
package org.koitharu.kotatsu.settings.sources.catalog
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.activity.viewModels
import androidx.appcompat.widget.PopupMenu
import androidx.appcompat.widget.SearchView
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.chip.Chip
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.combine
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.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.ui.widgets.ChipsView.ChipModel
import org.koitharu.kotatsu.core.util.LocaleComparator
import org.koitharu.kotatsu.core.util.ext.getDisplayName
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
@@ -25,7 +30,7 @@ import org.koitharu.kotatsu.databinding.ActivitySourcesCatalogBinding
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.settings.newsources.NewSourcesDialogFragment
import org.koitharu.kotatsu.search.ui.MangaListActivity
import javax.inject.Inject
@AndroidEntryPoint
@@ -36,8 +41,6 @@ class SourcesCatalogActivity : BaseActivity<ActivitySourcesCatalogBinding>(),
@Inject
lateinit var coil: ImageLoader
private var newSourcesSnackbar: Snackbar? = null
override val appBar: AppBarLayout
get() = viewBinding.appbar
@@ -55,16 +58,12 @@ class SourcesCatalogActivity : BaseActivity<ActivitySourcesCatalogBinding>(),
}
viewBinding.chipsFilter.onChipClickListener = this
viewModel.content.observe(this, sourcesAdapter)
viewModel.hasNewSources.observe(this, ::onHasNewSourcesChanged)
viewModel.onActionDone.observeEvent(
this,
ReversibleActionObserver(viewBinding.recyclerView),
)
viewModel.appliedFilter.observe(this) {
supportActionBar?.subtitle = it.locale?.toLocale().getDisplayName(this)
}
viewModel.filter.observe(this) {
viewBinding.chipsFilter.setChips(it)
combine(viewModel.appliedFilter, viewModel.hasNewSources, ::Pair).observe(this) {
updateFilers(it.first, it.second)
}
addMenuProvider(SourcesCatalogMenuProvider(this, viewModel, this))
}
@@ -79,11 +78,18 @@ class SourcesCatalogActivity : BaseActivity<ActivitySourcesCatalogBinding>(),
override fun onChipClick(chip: Chip, data: Any?) {
when (data) {
is ContentType -> viewModel.setContentType(data, chip.isChecked)
is Boolean -> viewModel.setNewOnly(chip.isChecked)
else -> showLocalesMenu(chip)
}
}
override fun onItemClick(item: SourceCatalogItem.Source, view: View) {
startActivity(MangaListActivity.newIntent(this, item.source))
}
override fun onItemLongClick(item: SourceCatalogItem.Source, view: View): Boolean {
viewModel.addSource(item.source)
return false
}
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
@@ -97,30 +103,52 @@ class SourcesCatalogActivity : BaseActivity<ActivitySourcesCatalogBinding>(),
return true
}
private fun onHasNewSourcesChanged(hasNewSources: Boolean) {
private fun updateFilers(
appliedFilter: SourcesCatalogFilter,
hasNewSources: Boolean,
) {
val chips = ArrayList<ChipModel>(ContentType.entries.size + 2)
chips += ChipModel(
title = appliedFilter.locale?.toLocale().getDisplayName(this),
icon = R.drawable.ic_language,
isDropdown = true,
)
if (hasNewSources) {
if (newSourcesSnackbar?.isShownOrQueued == true) {
return
}
val snackbar = Snackbar.make(viewBinding.recyclerView, R.string.new_sources_text, Snackbar.LENGTH_INDEFINITE)
snackbar.setAction(R.string.explore) {
NewSourcesDialogFragment.show(supportFragmentManager)
}
snackbar.addCallback(
object : Snackbar.Callback() {
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
super.onDismissed(transientBottomBar, event)
if (event == DISMISS_EVENT_SWIPE) {
viewModel.skipNewSources()
}
}
},
chips += ChipModel(
title = getString(R.string._new),
icon = R.drawable.ic_updated_selector,
isCheckable = true,
isChecked = appliedFilter.isNewOnly,
data = true,
)
snackbar.show()
newSourcesSnackbar = snackbar
} else {
newSourcesSnackbar?.dismiss()
newSourcesSnackbar = null
}
for (type in ContentType.entries) {
if (type == ContentType.HENTAI && viewModel.isNsfwDisabled) {
continue
}
chips += ChipModel(
title = getString(type.titleResId),
isCheckable = true,
isChecked = type in appliedFilter.types,
data = type,
)
}
viewBinding.chipsFilter.setChips(chips)
}
private fun showLocalesMenu(anchor: View) {
val locales = viewModel.locales.mapTo(ArrayList(viewModel.locales.size)) {
it to it?.toLocale()
}
locales.sortWith(compareBy(nullsFirst(LocaleComparator())) { it.second })
val menu = PopupMenu(this, anchor)
for ((i, lc) in locales.withIndex()) {
menu.menu.add(Menu.NONE, Menu.NONE, i, lc.second.getDisplayName(this))
}
menu.setOnMenuItemClickListener {
viewModel.setLocale(locales.getOrNull(it.order)?.first)
true
}
menu.show()
}
}

View File

@@ -1,9 +1,9 @@
package org.koitharu.kotatsu.settings.sources.catalog
import org.koitharu.kotatsu.parsers.model.ContentType
import java.util.Locale
data class SourcesCatalogFilter(
val types: Set<ContentType>,
val locale: String?,
val isNewOnly: Boolean,
)

View File

@@ -1,106 +0,0 @@
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
@Deprecated("")
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? = null
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()
when (val q = query) {
null -> sources.retainAll { it.contentType == contentType && it.locale == locale }
"" -> return emptyList()
else -> sources.retainAll { it.title.contains(q, ignoreCase = true) }
}
return if (sources.isEmpty()) {
listOf(
if (query == null) {
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,
showSummary = query != null,
)
}
}
}
@AssistedFactory
interface Factory {
fun create(
locale: String?,
contentType: ContentType,
lifecycle: ViewModelLifecycle,
): SourcesCatalogListProducer
}
}

View File

@@ -4,14 +4,9 @@ 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.core.util.LocaleComparator
import org.koitharu.kotatsu.core.util.ext.getDisplayName
import org.koitharu.kotatsu.core.util.ext.toLocale
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
class SourcesCatalogMenuProvider(
@@ -32,14 +27,7 @@ class SourcesCatalogMenuProvider(
searchView.queryHint = searchMenuItem.title
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
R.id.action_locales -> {
showLocalesMenu()
true
}
else -> false
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = false
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
(activity as? AppBarOwner)?.appBar?.setExpanded(false, true)
@@ -57,24 +45,4 @@ class SourcesCatalogMenuProvider(
viewModel.performSearch(newText?.trim().orEmpty())
return true
}
private fun showLocalesMenu() {
val locales = viewModel.locales.mapTo(ArrayList(viewModel.locales.size)) {
it to it?.toLocale()
}
locales.sortWith(compareBy(nullsFirst(LocaleComparator())) { it.second })
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()) {
menu.menu.add(Menu.NONE, Menu.NONE, i, lc.second.getDisplayName(activity))
}
menu.setOnMenuItemClickListener {
viewModel.setLocale(locales.getOrNull(it.order)?.first)
true
}
menu.show()
}
}

View File

@@ -7,16 +7,16 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.ui.widgets.ChipsView.ChipModel
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.explore.data.SourcesSortOrder
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mapToSet
@@ -27,6 +27,7 @@ import javax.inject.Inject
@HiltViewModel
class SourcesCatalogViewModel @Inject constructor(
private val repository: MangaSourcesRepository,
private val settings: AppSettings,
) : BaseViewModel() {
val onActionDone = MutableEventFlow<ReversibleAction>()
@@ -37,16 +38,14 @@ class SourcesCatalogViewModel @Inject constructor(
SourcesCatalogFilter(
types = emptySet(),
locale = Locale.getDefault().language.takeIf { it in locales },
isNewOnly = false,
),
)
val hasNewSources = repository.observeNewSources()
.map { it.isNotEmpty() }
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, false)
val isNsfwDisabled = settings.isNsfwContentDisabled
val filter: StateFlow<List<ChipModel>> = appliedFilter.map {
buildFilter(it)
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, buildFilter(appliedFilter.value))
val hasNewSources = repository.observeHasNewSources()
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, false)
val content: StateFlow<List<SourceCatalogItem>> = combine(
searchQuery,
@@ -55,6 +54,10 @@ class SourcesCatalogViewModel @Inject constructor(
buildSourcesList(f, q)
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
init {
repository.clearNewSourcesBadge()
}
fun performSearch(query: String?) {
searchQuery.value = query?.trim()
}
@@ -70,12 +73,6 @@ class SourcesCatalogViewModel @Inject constructor(
}
}
fun skipNewSources() {
launchJob {
repository.assimilateNewSources()
}
}
fun setContentType(value: ContentType, isAdd: Boolean) {
val filter = appliedFilter.value
val types = EnumSet.noneOf(ContentType::class.java)
@@ -88,29 +85,19 @@ class SourcesCatalogViewModel @Inject constructor(
appliedFilter.value = filter.copy(types = types)
}
private fun buildFilter(applied: SourcesCatalogFilter): List<ChipModel> = buildList(ContentType.entries.size) {
for (ct in ContentType.entries) {
add(
ChipModel(
tint = 0,
title = ct.name,
icon = 0,
isCheckable = true,
isChecked = ct in applied.types,
data = ct,
),
)
}
fun setNewOnly(value: Boolean) {
appliedFilter.value = appliedFilter.value.copy(isNewOnly = value)
}
private suspend fun buildSourcesList(filter: SourcesCatalogFilter, query: String?): List<SourceCatalogItem> {
val sources = repository.getDisabledSources().toMutableList()
sources.retainAll {
(filter.types.isEmpty() || it.contentType in filter.types) && it.locale == filter.locale
}
if (!query.isNullOrEmpty()) {
sources.retainAll { it.title.contains(query, ignoreCase = true) }
}
val sources = repository.getAvailableSources(
isDisabledOnly = true,
isNewOnly = filter.isNewOnly,
excludeBroken = false,
types = filter.types,
query = query,
sortOrder = SourcesSortOrder.ALPHABETIC,
).filter { it.locale == filter.locale }
return if (sources.isEmpty()) {
listOf(
if (query == null) {
@@ -128,12 +115,8 @@ class SourcesCatalogViewModel @Inject constructor(
},
)
} else {
sources.sortBy { it.title }
sources.map {
SourceCatalogItem.Source(
source = it,
showSummary = query != null,
)
SourceCatalogItem.Source(source = it)
}
}
}

View File

@@ -5,7 +5,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:windowBackground"
android:background="?selectableItemBackground"
android:gravity="center_vertical"
android:minHeight="?listPreferredItemHeightSmall"
android:orientation="horizontal"
@@ -52,10 +52,17 @@
</LinearLayout>
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:layout_marginVertical="4dp"
android:background="?colorOutline" />
<ImageView
android:id="@+id/imageView_add"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/list_spacing_small"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/add"
android:padding="@dimen/margin_small"

View File

@@ -3,12 +3,6 @@
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"

View File

@@ -650,4 +650,6 @@
<string name="disable_nsfw_notifications_summary">Do not show notifications about NSFW manga updates</string>
<string name="tracker_debug_info">Checking for new chapters log</string>
<string name="tracker_debug_info_summary">Debug information about background checks for new chapters</string>
<!-- In plural, used for filter -->
<string name="_new">New</string>
</resources>

View File

@@ -31,10 +31,4 @@
android:summary="@string/disable_nsfw_summary"
android:title="@string/disable_nsfw" />
<SwitchPreferenceCompat
android:defaultValue="true"
android:key="sources_new"
android:summary="@string/suggest_new_sources_summary"
android:title="@string/suggest_new_sources" />
</androidx.preference.PreferenceScreen>