Update sources catalog and repository
This commit is contained in:
@@ -16,8 +16,8 @@ android {
|
|||||||
applicationId 'org.koitharu.kotatsu'
|
applicationId 'org.koitharu.kotatsu'
|
||||||
minSdk = 21
|
minSdk = 21
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 645
|
versionCode = 646
|
||||||
versionName = '7.1.2'
|
versionName = '7.1.3'
|
||||||
generatedDensities = []
|
generatedDensities = []
|
||||||
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
|
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
|
||||||
ksp {
|
ksp {
|
||||||
@@ -82,7 +82,7 @@ afterEvaluate {
|
|||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
//noinspection GradleDependency
|
//noinspection GradleDependency
|
||||||
implementation('com.github.KotatsuApp:kotatsu-parsers:26be293f24') {
|
implementation('com.github.KotatsuApp:kotatsu-parsers:77a733a062') {
|
||||||
exclude group: 'org.json', module: 'json'
|
exclude group: 'org.json', module: 'json'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ abstract class MangaSourcesDao {
|
|||||||
@Query("SELECT source FROM sources WHERE enabled = 1")
|
@Query("SELECT source FROM sources WHERE enabled = 1")
|
||||||
abstract suspend fun findAllEnabledNames(): List<String>
|
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")
|
@Query("SELECT * FROM sources ORDER BY sort_key")
|
||||||
abstract fun observeAll(): Flow<List<MangaSourceEntity>>
|
abstract fun observeAll(): Flow<List<MangaSourceEntity>>
|
||||||
|
|
||||||
|
|||||||
@@ -290,8 +290,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
get() = prefs.getBoolean(KEY_SOURCES_GRID, true)
|
get() = prefs.getBoolean(KEY_SOURCES_GRID, true)
|
||||||
set(value) = prefs.edit { putBoolean(KEY_SOURCES_GRID, value) }
|
set(value) = prefs.edit { putBoolean(KEY_SOURCES_GRID, value) }
|
||||||
|
|
||||||
val isNewSourcesTipEnabled: Boolean
|
var sourcesVersion: Int
|
||||||
get() = prefs.getBoolean(KEY_SOURCES_NEW, true)
|
get() = prefs.getInt(KEY_SOURCES_VERSION, 0)
|
||||||
|
set(value) = prefs.edit { putInt(KEY_SOURCES_VERSION, value) }
|
||||||
|
|
||||||
val isPagesNumbersEnabled: Boolean
|
val isPagesNumbersEnabled: Boolean
|
||||||
get() = prefs.getBoolean(KEY_PAGES_NUMBERS, false)
|
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_APP_LOCALE = "app_locale"
|
||||||
const val KEY_LOGGING_ENABLED = "logging"
|
const val KEY_LOGGING_ENABLED = "logging"
|
||||||
const val KEY_SOURCES_GRID = "sources_grid"
|
const val KEY_SOURCES_GRID = "sources_grid"
|
||||||
const val KEY_SOURCES_NEW = "sources_new"
|
|
||||||
const val KEY_UPDATES_UNSTABLE = "updates_unstable"
|
const val KEY_UPDATES_UNSTABLE = "updates_unstable"
|
||||||
const val KEY_TIPS_CLOSED = "tips_closed"
|
const val KEY_TIPS_CLOSED = "tips_closed"
|
||||||
const val KEY_SSL_BYPASS = "ssl_bypass"
|
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_STATS_ENABLED = "stats_on"
|
||||||
const val KEY_FEED_HEADER = "feed_header"
|
const val KEY_FEED_HEADER = "feed_header"
|
||||||
const val KEY_SEARCH_SUGGESTION_TYPES = "search_suggest_types"
|
const val KEY_SEARCH_SUGGESTION_TYPES = "search_suggest_types"
|
||||||
|
const val KEY_SOURCES_VERSION = "sources_version"
|
||||||
|
|
||||||
// keys for non-persistent preferences
|
// keys for non-persistent preferences
|
||||||
const val KEY_APP_VERSION = "app_version"
|
const val KEY_APP_VERSION = "app_version"
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import com.google.android.material.chip.ChipGroup
|
|||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.util.ext.castOrNull
|
import org.koitharu.kotatsu.core.util.ext.castOrNull
|
||||||
|
|
||||||
|
import com.google.android.material.R as materialR
|
||||||
|
|
||||||
class ChipsView @JvmOverloads constructor(
|
class ChipsView @JvmOverloads constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
attrs: AttributeSet? = null,
|
attrs: AttributeSet? = null,
|
||||||
@@ -48,7 +50,7 @@ class ChipsView @JvmOverloads constructor(
|
|||||||
if (isInEditMode) {
|
if (isInEditMode) {
|
||||||
setChips(
|
setChips(
|
||||||
List(5) {
|
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.isChipIconVisible = true
|
||||||
}
|
}
|
||||||
chip.isChecked = model.isChecked
|
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
|
chip.tag = model.data
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,12 +117,11 @@ class ChipsView @JvmOverloads constructor(
|
|||||||
val chip = Chip(context)
|
val chip = Chip(context)
|
||||||
val drawable = ChipDrawable.createFromAttributes(context, null, 0, chipStyle)
|
val drawable = ChipDrawable.createFromAttributes(context, null, 0, chipStyle)
|
||||||
chip.setChipDrawable(drawable)
|
chip.setChipDrawable(drawable)
|
||||||
chip.isCheckedIconVisible = true
|
|
||||||
chip.isChipIconVisible = false
|
chip.isChipIconVisible = false
|
||||||
chip.isCloseIconVisible = onChipCloseClickListener != null
|
|
||||||
chip.setOnCloseIconClickListener(chipOnCloseListener)
|
chip.setOnCloseIconClickListener(chipOnCloseListener)
|
||||||
chip.setEnsureMinTouchTargetSize(false)
|
chip.setEnsureMinTouchTargetSize(false)
|
||||||
chip.setOnClickListener(chipOnClickListener)
|
chip.setOnClickListener(chipOnClickListener)
|
||||||
|
chip.isElegantTextHeight = false
|
||||||
addView(chip)
|
addView(chip)
|
||||||
return chip
|
return chip
|
||||||
}
|
}
|
||||||
@@ -127,11 +137,12 @@ class ChipsView @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
data class ChipModel(
|
data class ChipModel(
|
||||||
@ColorRes val tint: Int,
|
|
||||||
val title: CharSequence,
|
val title: CharSequence,
|
||||||
@DrawableRes val icon: Int,
|
@DrawableRes val icon: Int = 0,
|
||||||
val isCheckable: Boolean,
|
val isCheckable: Boolean = false,
|
||||||
val isChecked: Boolean,
|
@ColorRes val tint: Int = 0,
|
||||||
|
val isChecked: Boolean = false,
|
||||||
|
val isDropdown: Boolean = false,
|
||||||
val data: Any? = null,
|
val data: Any? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -612,10 +612,7 @@ class DetailsActivity :
|
|||||||
ChipsView.ChipModel(
|
ChipsView.ChipModel(
|
||||||
title = tag.title,
|
title = tag.title,
|
||||||
tint = tagHighlighter.getTagTint(tag),
|
tint = tagHighlighter.getTagTint(tag),
|
||||||
icon = 0,
|
|
||||||
data = tag,
|
data = tag,
|
||||||
isCheckable = false,
|
|
||||||
isChecked = false,
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,21 +6,22 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.flowOf
|
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onStart
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
import org.koitharu.kotatsu.core.db.dao.MangaSourcesDao
|
import org.koitharu.kotatsu.core.db.dao.MangaSourcesDao
|
||||||
import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity
|
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.model.isNsfw
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||||
import org.koitharu.kotatsu.core.ui.util.ReversibleHandle
|
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.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
import java.util.EnumSet
|
import java.util.EnumSet
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@Reusable
|
@Reusable
|
||||||
@@ -29,6 +30,7 @@ class MangaSourcesRepository @Inject constructor(
|
|||||||
private val settings: AppSettings,
|
private val settings: AppSettings,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
private val isNewSourcesAssimilated = AtomicBoolean(false)
|
||||||
private val dao: MangaSourcesDao
|
private val dao: MangaSourcesDao
|
||||||
get() = db.getSourcesDao()
|
get() = db.getSourcesDao()
|
||||||
|
|
||||||
@@ -43,25 +45,58 @@ class MangaSourcesRepository @Inject constructor(
|
|||||||
get() = Collections.unmodifiableSet(remoteSources)
|
get() = Collections.unmodifiableSet(remoteSources)
|
||||||
|
|
||||||
suspend fun getEnabledSources(): List<MangaSource> {
|
suspend fun getEnabledSources(): List<MangaSource> {
|
||||||
|
assimilateNewSources()
|
||||||
val order = settings.sourcesSortOrder
|
val order = settings.sourcesSortOrder
|
||||||
return dao.findAllEnabled(order).toSources(settings.isNsfwContentDisabled, order)
|
return dao.findAllEnabled(order).toSources(settings.isNsfwContentDisabled, order)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getDisabledSources(): Set<MangaSource> {
|
suspend fun getDisabledSources(): Set<MangaSource> {
|
||||||
|
assimilateNewSources()
|
||||||
val result = EnumSet.copyOf(remoteSources)
|
val result = EnumSet.copyOf(remoteSources)
|
||||||
val enabled = dao.findAllEnabledNames()
|
val enabled = dao.findAllEnabledNames()
|
||||||
for (name in enabled) {
|
for (name in enabled) {
|
||||||
val source = MangaSource(name)
|
val source = name.toMangaSourceOrNull() ?: continue
|
||||||
result.remove(source)
|
result.remove(source)
|
||||||
}
|
}
|
||||||
if (settings.isNsfwContentDisabled) {
|
|
||||||
result.removeAll { it.isNsfw() }
|
|
||||||
}
|
|
||||||
return result
|
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> {
|
fun observeIsEnabled(source: MangaSource): Flow<Boolean> {
|
||||||
return dao.observeIsEnabled(source.name)
|
return dao.observeIsEnabled(source.name).onStart { assimilateNewSources() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun observeEnabledSourcesCount(): Flow<Int> {
|
fun observeEnabledSourcesCount(): Flow<Int> {
|
||||||
@@ -69,8 +104,10 @@ class MangaSourcesRepository @Inject constructor(
|
|||||||
observeIsNsfwDisabled(),
|
observeIsNsfwDisabled(),
|
||||||
dao.observeEnabled(SourcesSortOrder.MANUAL),
|
dao.observeEnabled(SourcesSortOrder.MANUAL),
|
||||||
) { skipNsfw, sources ->
|
) { skipNsfw, sources ->
|
||||||
sources.count { !skipNsfw || !MangaSource(it.source).isNsfw() }
|
sources.count {
|
||||||
}.distinctUntilChanged()
|
it.source.toMangaSourceOrNull()?.let { s -> !skipNsfw || !s.isNsfw() } == true
|
||||||
|
}
|
||||||
|
}.distinctUntilChanged().onStart { assimilateNewSources() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun observeAvailableSourcesCount(): Flow<Int> {
|
fun observeAvailableSourcesCount(): Flow<Int> {
|
||||||
@@ -82,7 +119,7 @@ class MangaSourcesRepository @Inject constructor(
|
|||||||
allMangaSources.count { x ->
|
allMangaSources.count { x ->
|
||||||
x.name !in enabled && (!skipNsfw || !x.isNsfw())
|
x.name !in enabled && (!skipNsfw || !x.isNsfw())
|
||||||
}
|
}
|
||||||
}.distinctUntilChanged()
|
}.distinctUntilChanged().onStart { assimilateNewSources() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun observeEnabledSources(): Flow<List<MangaSource>> = combine(
|
fun observeEnabledSources(): Flow<List<MangaSource>> = combine(
|
||||||
@@ -92,18 +129,18 @@ class MangaSourcesRepository @Inject constructor(
|
|||||||
dao.observeEnabled(order).map {
|
dao.observeEnabled(order).map {
|
||||||
it.toSources(skipNsfw, order)
|
it.toSources(skipNsfw, order)
|
||||||
}
|
}
|
||||||
}.flatMapLatest { it }
|
}.flatMapLatest { it }.onStart { assimilateNewSources() }
|
||||||
|
|
||||||
fun observeAll(): Flow<List<Pair<MangaSource, Boolean>>> = dao.observeAll().map { entities ->
|
fun observeAll(): Flow<List<Pair<MangaSource, Boolean>>> = dao.observeAll().map { entities ->
|
||||||
val result = ArrayList<Pair<MangaSource, Boolean>>(entities.size)
|
val result = ArrayList<Pair<MangaSource, Boolean>>(entities.size)
|
||||||
for (entity in entities) {
|
for (entity in entities) {
|
||||||
val source = MangaSource(entity.source)
|
val source = entity.source.toMangaSourceOrNull() ?: continue
|
||||||
if (source in remoteSources) {
|
if (source in remoteSources) {
|
||||||
result.add(source to entity.isEnabled)
|
result.add(source to entity.isEnabled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
}
|
}.onStart { assimilateNewSources() }
|
||||||
|
|
||||||
suspend fun setSourcesEnabled(sources: Collection<MangaSource>, isEnabled: Boolean): ReversibleHandle {
|
suspend fun setSourcesEnabled(sources: Collection<MangaSource>, isEnabled: Boolean): ReversibleHandle {
|
||||||
setSourcesEnabledImpl(sources, isEnabled)
|
setSourcesEnabledImpl(sources, isEnabled)
|
||||||
@@ -114,6 +151,7 @@ class MangaSourcesRepository @Inject constructor(
|
|||||||
|
|
||||||
suspend fun setSourcesEnabledExclusive(sources: Set<MangaSource>) {
|
suspend fun setSourcesEnabledExclusive(sources: Set<MangaSource>) {
|
||||||
db.withTransaction {
|
db.withTransaction {
|
||||||
|
assimilateNewSources()
|
||||||
for (s in remoteSources) {
|
for (s in remoteSources) {
|
||||||
dao.setEnabled(s.name, s in sources)
|
dao.setEnabled(s.name, s in sources)
|
||||||
}
|
}
|
||||||
@@ -135,32 +173,34 @@ class MangaSourcesRepository @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun observeNewSources(): Flow<Set<MangaSource>> = observeIsNewSourcesEnabled().flatMapLatest {
|
fun observeHasNewSources(): Flow<Boolean> = observeIsNsfwDisabled().map { skipNsfw ->
|
||||||
if (it) {
|
val sources = dao.findAllFromVersion(BuildConfig.VERSION_CODE).toSources(skipNsfw, null)
|
||||||
combine(
|
sources.isNotEmpty()
|
||||||
dao.observeAll(),
|
}.onStart { assimilateNewSources() }
|
||||||
observeIsNsfwDisabled(),
|
|
||||||
) { entities, skipNsfw ->
|
fun observeHasNewSourcesForBadge(): Flow<Boolean> = combine(
|
||||||
val result = EnumSet.copyOf(remoteSources)
|
settings.observeAsFlow(AppSettings.KEY_SOURCES_VERSION) { sourcesVersion },
|
||||||
for (e in entities) {
|
observeIsNsfwDisabled(),
|
||||||
result.remove(MangaSource(e.source))
|
) { version, skipNsfw ->
|
||||||
}
|
if (version < BuildConfig.VERSION_CODE) {
|
||||||
if (skipNsfw) {
|
val sources = dao.findAllFromVersion(version).toSources(skipNsfw, null)
|
||||||
result.removeAll { x -> x.isNsfw() }
|
sources.isNotEmpty()
|
||||||
}
|
|
||||||
result
|
|
||||||
}.distinctUntilChanged()
|
|
||||||
} else {
|
} else {
|
||||||
assimilateNewSources()
|
false
|
||||||
flowOf(emptySet())
|
|
||||||
}
|
}
|
||||||
|
}.onStart { assimilateNewSources() }
|
||||||
|
|
||||||
|
fun clearNewSourcesBadge() {
|
||||||
|
settings.sourcesVersion = BuildConfig.VERSION_CODE
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated("")
|
private suspend fun assimilateNewSources(): Boolean {
|
||||||
suspend fun assimilateNewSources(): Set<MangaSource> {
|
if (isNewSourcesAssimilated.getAndSet(true)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
val new = getNewSources()
|
val new = getNewSources()
|
||||||
if (new.isEmpty()) {
|
if (new.isEmpty()) {
|
||||||
return emptySet()
|
return false
|
||||||
}
|
}
|
||||||
var maxSortKey = dao.getMaxSortKey()
|
var maxSortKey = dao.getMaxSortKey()
|
||||||
val entities = new.map { x ->
|
val entities = new.map { x ->
|
||||||
@@ -172,14 +212,11 @@ class MangaSourcesRepository @Inject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
dao.insertIfAbsent(entities)
|
dao.insertIfAbsent(entities)
|
||||||
if (settings.isNsfwContentDisabled) {
|
return true
|
||||||
new.removeAll { x -> x.isNsfw() }
|
|
||||||
}
|
|
||||||
return new
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun isSetupRequired(): Boolean {
|
suspend fun isSetupRequired(): Boolean {
|
||||||
return dao.findAll().isEmpty()
|
return settings.sourcesVersion == 0 && dao.findAllEnabledNames().isEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun setSourcesEnabledImpl(sources: Collection<MangaSource>, isEnabled: Boolean) {
|
private suspend fun setSourcesEnabledImpl(sources: Collection<MangaSource>, isEnabled: Boolean) {
|
||||||
@@ -198,7 +235,7 @@ class MangaSourcesRepository @Inject constructor(
|
|||||||
val entities = dao.findAll()
|
val entities = dao.findAll()
|
||||||
val result = EnumSet.copyOf(remoteSources)
|
val result = EnumSet.copyOf(remoteSources)
|
||||||
for (e in entities) {
|
for (e in entities) {
|
||||||
result.remove(MangaSource(e.source))
|
result.remove(e.source.toMangaSourceOrNull() ?: continue)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -206,10 +243,10 @@ class MangaSourcesRepository @Inject constructor(
|
|||||||
private fun List<MangaSourceEntity>.toSources(
|
private fun List<MangaSourceEntity>.toSources(
|
||||||
skipNsfwSources: Boolean,
|
skipNsfwSources: Boolean,
|
||||||
sortOrder: SourcesSortOrder?,
|
sortOrder: SourcesSortOrder?,
|
||||||
): List<MangaSource> {
|
): MutableList<MangaSource> {
|
||||||
val result = ArrayList<MangaSource>(size)
|
val result = ArrayList<MangaSource>(size)
|
||||||
for (entity in this) {
|
for (entity in this) {
|
||||||
val source = MangaSource(entity.source)
|
val source = entity.source.toMangaSourceOrNull() ?: continue
|
||||||
if (skipNsfwSources && source.isNsfw()) {
|
if (skipNsfwSources && source.isNsfw()) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -227,11 +264,9 @@ class MangaSourcesRepository @Inject constructor(
|
|||||||
isNsfwContentDisabled
|
isNsfwContentDisabled
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun observeIsNewSourcesEnabled() = settings.observeAsFlow(AppSettings.KEY_SOURCES_NEW) {
|
|
||||||
isNewSourcesTipEnabled
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun observeSortOrder() = settings.observeAsFlow(AppSettings.KEY_SOURCES_ORDER) {
|
private fun observeSortOrder() = settings.observeAsFlow(AppSettings.KEY_SOURCES_ORDER) {
|
||||||
sourcesSortOrder
|
sourcesSortOrder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun String.toMangaSourceOrNull(): MangaSource? = MangaSource.entries.find { it.name == this }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.RecyclerViewOwner
|
||||||
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
|
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
|
||||||
import org.koitharu.kotatsu.core.ui.util.SpanSizeResolver
|
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.addMenuProvider
|
||||||
import org.koitharu.kotatsu.core.util.ext.findAppCompatDelegate
|
import org.koitharu.kotatsu.core.util.ext.findAppCompatDelegate
|
||||||
import org.koitharu.kotatsu.core.util.ext.observe
|
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.explore.ui.model.MangaSourceItem
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
|
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
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.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
|
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
|
||||||
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
||||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
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.settings.sources.catalog.SourcesCatalogActivity
|
||||||
import org.koitharu.kotatsu.suggestions.ui.SuggestionsActivity
|
import org.koitharu.kotatsu.suggestions.ui.SuggestionsActivity
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -56,7 +53,7 @@ class ExploreFragment :
|
|||||||
BaseFragment<FragmentExploreBinding>(),
|
BaseFragment<FragmentExploreBinding>(),
|
||||||
RecyclerViewOwner,
|
RecyclerViewOwner,
|
||||||
ExploreListEventListener,
|
ExploreListEventListener,
|
||||||
OnListItemClickListener<MangaSourceItem>, TipView.OnButtonClickListener, ListSelectionController.Callback2 {
|
OnListItemClickListener<MangaSourceItem>, ListSelectionController.Callback2 {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var coil: ImageLoader
|
lateinit var coil: ImageLoader
|
||||||
@@ -74,7 +71,7 @@ class ExploreFragment :
|
|||||||
|
|
||||||
override fun onViewBindingCreated(binding: FragmentExploreBinding, savedInstanceState: Bundle?) {
|
override fun onViewBindingCreated(binding: FragmentExploreBinding, savedInstanceState: Bundle?) {
|
||||||
super.onViewBindingCreated(binding, savedInstanceState)
|
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))
|
startActivity(DetailsActivity.newIntent(view.context, manga))
|
||||||
}
|
}
|
||||||
sourceSelectionController = ListSelectionController(
|
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) {
|
override fun onClick(v: View) {
|
||||||
val intent = when (v.id) {
|
val intent = when (v.id) {
|
||||||
R.id.button_local -> MangaListActivity.newIntent(v.context, MangaSource.LOCAL)
|
R.id.button_local -> MangaListActivity.newIntent(v.context, MangaSource.LOCAL)
|
||||||
|
|||||||
@@ -102,12 +102,6 @@ class ExploreViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun discardNewSources() {
|
|
||||||
launchJob(Dispatchers.Default) {
|
|
||||||
sourcesRepository.assimilateNewSources()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun requestPinShortcut(source: MangaSource) {
|
fun requestPinShortcut(source: MangaSource) {
|
||||||
launchLoadingJob(Dispatchers.Default) {
|
launchLoadingJob(Dispatchers.Default) {
|
||||||
shortcutManager.requestPinShortcut(source)
|
shortcutManager.requestPinShortcut(source)
|
||||||
@@ -124,7 +118,7 @@ class ExploreViewModel @Inject constructor(
|
|||||||
getSuggestionFlow(),
|
getSuggestionFlow(),
|
||||||
isGrid,
|
isGrid,
|
||||||
isRandomLoading,
|
isRandomLoading,
|
||||||
sourcesRepository.observeNewSources(),
|
sourcesRepository.observeHasNewSourcesForBadge(),
|
||||||
) { content, suggestions, grid, randomLoading, newSources ->
|
) { content, suggestions, grid, randomLoading, newSources ->
|
||||||
buildList(content, suggestions, grid, randomLoading, newSources)
|
buildList(content, suggestions, grid, randomLoading, newSources)
|
||||||
}.withErrorHandling()
|
}.withErrorHandling()
|
||||||
@@ -134,7 +128,7 @@ class ExploreViewModel @Inject constructor(
|
|||||||
recommendation: List<Manga>,
|
recommendation: List<Manga>,
|
||||||
isGrid: Boolean,
|
isGrid: Boolean,
|
||||||
randomLoading: Boolean,
|
randomLoading: Boolean,
|
||||||
newSources: Set<MangaSource>,
|
hasNewSources: Boolean,
|
||||||
): List<ListModel> {
|
): List<ListModel> {
|
||||||
val result = ArrayList<ListModel>(sources.size + 3)
|
val result = ArrayList<ListModel>(sources.size + 3)
|
||||||
result += ExploreButtons(randomLoading)
|
result += ExploreButtons(randomLoading)
|
||||||
@@ -146,7 +140,7 @@ class ExploreViewModel @Inject constructor(
|
|||||||
result += ListHeader(
|
result += ListHeader(
|
||||||
textRes = R.string.remote_sources,
|
textRes = R.string.remote_sources,
|
||||||
buttonTextRes = R.string.catalog,
|
buttonTextRes = R.string.catalog,
|
||||||
badge = if (newSources.isNotEmpty()) "" else null,
|
badge = if (hasNewSources) "" else null,
|
||||||
)
|
)
|
||||||
sources.mapTo(result) { MangaSourceItem(it, isGrid) }
|
sources.mapTo(result) { MangaSourceItem(it, isGrid) }
|
||||||
} else {
|
} else {
|
||||||
@@ -191,6 +185,5 @@ class ExploreViewModel @Inject constructor(
|
|||||||
|
|
||||||
private const val TIP_SUGGESTIONS = "suggestions"
|
private const val TIP_SUGGESTIONS = "suggestions"
|
||||||
private const val SUGGESTIONS_COUNT = 8
|
private const val SUGGESTIONS_COUNT = 8
|
||||||
const val TIP_NEW_SOURCES = "new_sources"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,11 @@ import androidx.lifecycle.LifecycleOwner
|
|||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import org.koitharu.kotatsu.core.ui.BaseListAdapter
|
import org.koitharu.kotatsu.core.ui.BaseListAdapter
|
||||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
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.explore.ui.model.MangaSourceItem
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.emptyHintAD
|
import org.koitharu.kotatsu.list.ui.adapter.emptyHintAD
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.listHeaderAD
|
import org.koitharu.kotatsu.list.ui.adapter.listHeaderAD
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
|
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.list.ui.model.ListModel
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
|
||||||
@@ -18,7 +16,6 @@ class ExploreAdapter(
|
|||||||
coil: ImageLoader,
|
coil: ImageLoader,
|
||||||
lifecycleOwner: LifecycleOwner,
|
lifecycleOwner: LifecycleOwner,
|
||||||
listener: ExploreListEventListener,
|
listener: ExploreListEventListener,
|
||||||
tipClickListener: TipView.OnButtonClickListener,
|
|
||||||
clickListener: OnListItemClickListener<MangaSourceItem>,
|
clickListener: OnListItemClickListener<MangaSourceItem>,
|
||||||
mangaClickListener: OnListItemClickListener<Manga>,
|
mangaClickListener: OnListItemClickListener<Manga>,
|
||||||
) : BaseListAdapter<ListModel>() {
|
) : BaseListAdapter<ListModel>() {
|
||||||
@@ -34,6 +31,5 @@ class ExploreAdapter(
|
|||||||
addDelegate(ListItemType.EXPLORE_SOURCE_GRID, exploreSourceGridItemAD(coil, clickListener, lifecycleOwner))
|
addDelegate(ListItemType.EXPLORE_SOURCE_GRID, exploreSourceGridItemAD(coil, clickListener, lifecycleOwner))
|
||||||
addDelegate(ListItemType.HINT_EMPTY, emptyHintAD(coil, lifecycleOwner, listener))
|
addDelegate(ListItemType.HINT_EMPTY, emptyHintAD(coil, lifecycleOwner, listener))
|
||||||
addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
|
addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
|
||||||
addDelegate(ListItemType.TIP, tipAD(tipClickListener))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -257,7 +257,7 @@ class FilterCoordinator @Inject constructor(
|
|||||||
}
|
}
|
||||||
oldValue.copy(
|
oldValue.copy(
|
||||||
tagsExclude = newTags,
|
tagsExclude = newTags,
|
||||||
tags = oldValue.tags - newTags
|
tags = oldValue.tags - newTags,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -308,7 +308,7 @@ class FilterCoordinator @Inject constructor(
|
|||||||
currentState.update { oldValue ->
|
currentState.update { oldValue ->
|
||||||
oldValue.copy(
|
oldValue.copy(
|
||||||
tags = tags,
|
tags = tags,
|
||||||
tagsExclude = oldValue.tagsExclude - tags
|
tagsExclude = oldValue.tagsExclude - tags,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -391,9 +391,7 @@ class FilterCoordinator @Inject constructor(
|
|||||||
val result = LinkedList<ChipsView.ChipModel>()
|
val result = LinkedList<ChipsView.ChipModel>()
|
||||||
for (tag in tags) {
|
for (tag in tags) {
|
||||||
val model = ChipsView.ChipModel(
|
val model = ChipsView.ChipModel(
|
||||||
tint = 0,
|
|
||||||
title = tag.title,
|
title = tag.title,
|
||||||
icon = 0,
|
|
||||||
isCheckable = true,
|
isCheckable = true,
|
||||||
isChecked = selectedTags.remove(tag),
|
isChecked = selectedTags.remove(tag),
|
||||||
data = tag,
|
data = tag,
|
||||||
@@ -406,9 +404,7 @@ class FilterCoordinator @Inject constructor(
|
|||||||
}
|
}
|
||||||
for (tag in selectedTags) {
|
for (tag in selectedTags) {
|
||||||
val model = ChipsView.ChipModel(
|
val model = ChipsView.ChipModel(
|
||||||
tint = 0,
|
|
||||||
title = tag.title,
|
title = tag.title,
|
||||||
icon = 0,
|
|
||||||
isCheckable = true,
|
isCheckable = true,
|
||||||
isChecked = true,
|
isChecked = true,
|
||||||
data = tag,
|
data = tag,
|
||||||
|
|||||||
@@ -61,10 +61,7 @@ class FilterHeaderFragment : BaseFragment<FragmentFilterHeaderBinding>(), ChipsV
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun moreTagsChip() = ChipsView.ChipModel(
|
private fun moreTagsChip() = ChipsView.ChipModel(
|
||||||
tint = 0,
|
|
||||||
title = getString(R.string.more),
|
title = getString(R.string.more),
|
||||||
icon = materialR.drawable.abc_ic_menu_overflow_material,
|
icon = materialR.drawable.abc_ic_menu_overflow_material,
|
||||||
isCheckable = false,
|
|
||||||
isChecked = false,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,9 +144,7 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
|
|||||||
val chips = ArrayList<ChipsView.ChipModel>(value.selectedItems.size + value.availableItems.size + 1)
|
val chips = ArrayList<ChipsView.ChipModel>(value.selectedItems.size + value.availableItems.size + 1)
|
||||||
value.selectedItems.mapTo(chips) { tag ->
|
value.selectedItems.mapTo(chips) { tag ->
|
||||||
ChipsView.ChipModel(
|
ChipsView.ChipModel(
|
||||||
tint = 0,
|
|
||||||
title = tag.title,
|
title = tag.title,
|
||||||
icon = 0,
|
|
||||||
isCheckable = true,
|
isCheckable = true,
|
||||||
isChecked = true,
|
isChecked = true,
|
||||||
data = tag,
|
data = tag,
|
||||||
@@ -155,9 +153,7 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
|
|||||||
value.availableItems.mapNotNullTo(chips) { tag ->
|
value.availableItems.mapNotNullTo(chips) { tag ->
|
||||||
if (tag !in value.selectedItems) {
|
if (tag !in value.selectedItems) {
|
||||||
ChipsView.ChipModel(
|
ChipsView.ChipModel(
|
||||||
tint = 0,
|
|
||||||
title = tag.title,
|
title = tag.title,
|
||||||
icon = 0,
|
|
||||||
isCheckable = true,
|
isCheckable = true,
|
||||||
isChecked = false,
|
isChecked = false,
|
||||||
data = tag,
|
data = tag,
|
||||||
@@ -168,12 +164,8 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
|
|||||||
}
|
}
|
||||||
chips.add(
|
chips.add(
|
||||||
ChipsView.ChipModel(
|
ChipsView.ChipModel(
|
||||||
tint = 0,
|
|
||||||
title = getString(R.string.more),
|
title = getString(R.string.more),
|
||||||
icon = materialR.drawable.abc_ic_menu_overflow_material,
|
icon = materialR.drawable.abc_ic_menu_overflow_material,
|
||||||
isCheckable = false,
|
|
||||||
isChecked = false,
|
|
||||||
data = null,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
b.chipsGenres.setChips(chips)
|
b.chipsGenres.setChips(chips)
|
||||||
@@ -200,9 +192,7 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
|
|||||||
value.availableItems.mapNotNullTo(chips) { tag ->
|
value.availableItems.mapNotNullTo(chips) { tag ->
|
||||||
if (tag !in value.selectedItems) {
|
if (tag !in value.selectedItems) {
|
||||||
ChipsView.ChipModel(
|
ChipsView.ChipModel(
|
||||||
tint = 0,
|
|
||||||
title = tag.title,
|
title = tag.title,
|
||||||
icon = 0,
|
|
||||||
isCheckable = true,
|
isCheckable = true,
|
||||||
isChecked = false,
|
isChecked = false,
|
||||||
data = tag,
|
data = tag,
|
||||||
@@ -213,12 +203,8 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
|
|||||||
}
|
}
|
||||||
chips.add(
|
chips.add(
|
||||||
ChipsView.ChipModel(
|
ChipsView.ChipModel(
|
||||||
tint = 0,
|
|
||||||
title = getString(R.string.more),
|
title = getString(R.string.more),
|
||||||
icon = materialR.drawable.abc_ic_menu_overflow_material,
|
icon = materialR.drawable.abc_ic_menu_overflow_material,
|
||||||
isCheckable = false,
|
|
||||||
isChecked = false,
|
|
||||||
data = null,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
b.chipsGenresExclude.setChips(chips)
|
b.chipsGenresExclude.setChips(chips)
|
||||||
@@ -233,9 +219,7 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
|
|||||||
}
|
}
|
||||||
val chips = value.availableItems.map { state ->
|
val chips = value.availableItems.map { state ->
|
||||||
ChipsView.ChipModel(
|
ChipsView.ChipModel(
|
||||||
tint = 0,
|
|
||||||
title = getString(state.titleResId),
|
title = getString(state.titleResId),
|
||||||
icon = 0,
|
|
||||||
isCheckable = true,
|
isCheckable = true,
|
||||||
isChecked = state in value.selectedItems,
|
isChecked = state in value.selectedItems,
|
||||||
data = state,
|
data = state,
|
||||||
@@ -253,9 +237,7 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
|
|||||||
}
|
}
|
||||||
val chips = value.availableItems.map { contentRating ->
|
val chips = value.availableItems.map { contentRating ->
|
||||||
ChipsView.ChipModel(
|
ChipsView.ChipModel(
|
||||||
tint = 0,
|
|
||||||
title = getString(contentRating.titleResId),
|
title = getString(contentRating.titleResId),
|
||||||
icon = 0,
|
|
||||||
isCheckable = true,
|
isCheckable = true,
|
||||||
isChecked = contentRating in value.selectedItems,
|
isChecked = contentRating in value.selectedItems,
|
||||||
data = contentRating,
|
data = contentRating,
|
||||||
|
|||||||
@@ -37,9 +37,6 @@ suspend fun Manga.toListDetailedModel(
|
|||||||
ChipsView.ChipModel(
|
ChipsView.ChipModel(
|
||||||
tint = extraProvider?.getTagTint(it) ?: 0,
|
tint = extraProvider?.getTagTint(it) ?: 0,
|
||||||
title = it.title,
|
title = it.title,
|
||||||
icon = 0,
|
|
||||||
isCheckable = false,
|
|
||||||
isChecked = false,
|
|
||||||
data = it,
|
data = it,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -85,10 +85,7 @@ class PreviewViewModel @Inject constructor(
|
|||||||
ChipsView.ChipModel(
|
ChipsView.ChipModel(
|
||||||
title = tag.title,
|
title = tag.title,
|
||||||
tint = extraProvider.getTagTint(tag),
|
tint = extraProvider.getTagTint(tag),
|
||||||
icon = 0,
|
|
||||||
data = tag,
|
data = tag,
|
||||||
isCheckable = false,
|
|
||||||
isChecked = false,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
|
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
|
||||||
|
|||||||
@@ -91,9 +91,7 @@ class WelcomeSheet : BaseAdaptiveSheet<SheetWelcomeBinding>(), ChipsView.OnChipC
|
|||||||
chips.setChips(
|
chips.setChips(
|
||||||
value.availableItems.map {
|
value.availableItems.map {
|
||||||
ChipsView.ChipModel(
|
ChipsView.ChipModel(
|
||||||
tint = 0,
|
|
||||||
title = it?.getDisplayLanguage(it)?.toTitleCase(it) ?: getString(R.string.various_languages),
|
title = it?.getDisplayLanguage(it)?.toTitleCase(it) ?: getString(R.string.various_languages),
|
||||||
icon = 0,
|
|
||||||
isCheckable = true,
|
isCheckable = true,
|
||||||
isChecked = it in value.selectedItems,
|
isChecked = it in value.selectedItems,
|
||||||
data = it,
|
data = it,
|
||||||
@@ -107,9 +105,7 @@ class WelcomeSheet : BaseAdaptiveSheet<SheetWelcomeBinding>(), ChipsView.OnChipC
|
|||||||
chips.setChips(
|
chips.setChips(
|
||||||
value.availableItems.map {
|
value.availableItems.map {
|
||||||
ChipsView.ChipModel(
|
ChipsView.ChipModel(
|
||||||
tint = 0,
|
|
||||||
title = getString(it.titleResId),
|
title = getString(it.titleResId),
|
||||||
icon = 0,
|
|
||||||
isCheckable = true,
|
isCheckable = true,
|
||||||
isChecked = it in value.selectedItems,
|
isChecked = it in value.selectedItems,
|
||||||
data = it,
|
data = it,
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ class WelcomeViewModel @Inject constructor(
|
|||||||
selectedItems = selectedLocales,
|
selectedItems = selectedLocales,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
)
|
)
|
||||||
repository.assimilateNewSources()
|
repository.clearNewSourcesBadge()
|
||||||
commit()
|
commit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -172,12 +172,8 @@ class SearchSuggestionViewModel @Inject constructor(
|
|||||||
|
|
||||||
private fun mapTags(tags: List<MangaTag>): List<ChipsView.ChipModel> = tags.map { tag ->
|
private fun mapTags(tags: List<MangaTag>): List<ChipsView.ChipModel> = tags.map { tag ->
|
||||||
ChipsView.ChipModel(
|
ChipsView.ChipModel(
|
||||||
tint = 0,
|
|
||||||
title = tag.title,
|
title = tag.title,
|
||||||
icon = 0,
|
|
||||||
data = tag,
|
data = tag,
|
||||||
isCheckable = false,
|
|
||||||
isChecked = false,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,7 +9,6 @@ sealed interface SourceCatalogItem : ListModel {
|
|||||||
|
|
||||||
data class Source(
|
data class Source(
|
||||||
val source: MangaSource,
|
val source: MangaSource,
|
||||||
val showSummary: Boolean,
|
|
||||||
) : SourceCatalogItem {
|
) : SourceCatalogItem {
|
||||||
|
|
||||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||||
|
|||||||
@@ -34,17 +34,15 @@ fun sourceCatalogItemSourceAD(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
binding.imageViewAdd.setOnClickListener { v ->
|
binding.imageViewAdd.setOnClickListener { v ->
|
||||||
|
listener.onItemLongClick(item, v)
|
||||||
|
}
|
||||||
|
binding.root.setOnClickListener { v ->
|
||||||
listener.onItemClick(item, v)
|
listener.onItemClick(item, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
bind {
|
bind {
|
||||||
binding.textViewTitle.text = item.source.getTitle(context)
|
binding.textViewTitle.text = item.source.getTitle(context)
|
||||||
if (item.showSummary) {
|
binding.textViewDescription.text = item.source.getSummary(context)
|
||||||
binding.textViewDescription.text = item.source.getSummary(context)
|
|
||||||
binding.textViewDescription.isVisible = true
|
|
||||||
} else {
|
|
||||||
binding.textViewDescription.isVisible = false
|
|
||||||
}
|
|
||||||
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
|
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
|
||||||
binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
|
binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
|
||||||
crossfade(context)
|
crossfade(context)
|
||||||
|
|||||||
@@ -1,22 +1,27 @@
|
|||||||
package org.koitharu.kotatsu.settings.sources.catalog
|
package org.koitharu.kotatsu.settings.sources.catalog
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
|
import androidx.appcompat.widget.PopupMenu
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
import androidx.core.graphics.Insets
|
import androidx.core.graphics.Insets
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
import com.google.android.material.chip.Chip
|
import com.google.android.material.chip.Chip
|
||||||
import com.google.android.material.snackbar.Snackbar
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
import org.koitharu.kotatsu.R
|
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.BaseActivity
|
||||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||||
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
|
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
|
||||||
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
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.getDisplayName
|
||||||
import org.koitharu.kotatsu.core.util.ext.observe
|
import org.koitharu.kotatsu.core.util.ext.observe
|
||||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
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.list.ui.adapter.TypedListSpacingDecoration
|
||||||
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
||||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
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
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
@@ -36,8 +41,6 @@ class SourcesCatalogActivity : BaseActivity<ActivitySourcesCatalogBinding>(),
|
|||||||
@Inject
|
@Inject
|
||||||
lateinit var coil: ImageLoader
|
lateinit var coil: ImageLoader
|
||||||
|
|
||||||
private var newSourcesSnackbar: Snackbar? = null
|
|
||||||
|
|
||||||
override val appBar: AppBarLayout
|
override val appBar: AppBarLayout
|
||||||
get() = viewBinding.appbar
|
get() = viewBinding.appbar
|
||||||
|
|
||||||
@@ -55,16 +58,12 @@ class SourcesCatalogActivity : BaseActivity<ActivitySourcesCatalogBinding>(),
|
|||||||
}
|
}
|
||||||
viewBinding.chipsFilter.onChipClickListener = this
|
viewBinding.chipsFilter.onChipClickListener = this
|
||||||
viewModel.content.observe(this, sourcesAdapter)
|
viewModel.content.observe(this, sourcesAdapter)
|
||||||
viewModel.hasNewSources.observe(this, ::onHasNewSourcesChanged)
|
|
||||||
viewModel.onActionDone.observeEvent(
|
viewModel.onActionDone.observeEvent(
|
||||||
this,
|
this,
|
||||||
ReversibleActionObserver(viewBinding.recyclerView),
|
ReversibleActionObserver(viewBinding.recyclerView),
|
||||||
)
|
)
|
||||||
viewModel.appliedFilter.observe(this) {
|
combine(viewModel.appliedFilter, viewModel.hasNewSources, ::Pair).observe(this) {
|
||||||
supportActionBar?.subtitle = it.locale?.toLocale().getDisplayName(this)
|
updateFilers(it.first, it.second)
|
||||||
}
|
|
||||||
viewModel.filter.observe(this) {
|
|
||||||
viewBinding.chipsFilter.setChips(it)
|
|
||||||
}
|
}
|
||||||
addMenuProvider(SourcesCatalogMenuProvider(this, viewModel, this))
|
addMenuProvider(SourcesCatalogMenuProvider(this, viewModel, this))
|
||||||
}
|
}
|
||||||
@@ -79,11 +78,18 @@ class SourcesCatalogActivity : BaseActivity<ActivitySourcesCatalogBinding>(),
|
|||||||
override fun onChipClick(chip: Chip, data: Any?) {
|
override fun onChipClick(chip: Chip, data: Any?) {
|
||||||
when (data) {
|
when (data) {
|
||||||
is ContentType -> viewModel.setContentType(data, chip.isChecked)
|
is ContentType -> viewModel.setContentType(data, chip.isChecked)
|
||||||
|
is Boolean -> viewModel.setNewOnly(chip.isChecked)
|
||||||
|
else -> showLocalesMenu(chip)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemClick(item: SourceCatalogItem.Source, view: View) {
|
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)
|
viewModel.addSource(item.source)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||||
@@ -97,30 +103,52 @@ class SourcesCatalogActivity : BaseActivity<ActivitySourcesCatalogBinding>(),
|
|||||||
return true
|
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 (hasNewSources) {
|
||||||
if (newSourcesSnackbar?.isShownOrQueued == true) {
|
chips += ChipModel(
|
||||||
return
|
title = getString(R.string._new),
|
||||||
}
|
icon = R.drawable.ic_updated_selector,
|
||||||
val snackbar = Snackbar.make(viewBinding.recyclerView, R.string.new_sources_text, Snackbar.LENGTH_INDEFINITE)
|
isCheckable = true,
|
||||||
snackbar.setAction(R.string.explore) {
|
isChecked = appliedFilter.isNewOnly,
|
||||||
NewSourcesDialogFragment.show(supportFragmentManager)
|
data = true,
|
||||||
}
|
|
||||||
snackbar.addCallback(
|
|
||||||
object : Snackbar.Callback() {
|
|
||||||
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
|
|
||||||
super.onDismissed(transientBottomBar, event)
|
|
||||||
if (event == DISMISS_EVENT_SWIPE) {
|
|
||||||
viewModel.skipNewSources()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package org.koitharu.kotatsu.settings.sources.catalog
|
package org.koitharu.kotatsu.settings.sources.catalog
|
||||||
|
|
||||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
data class SourcesCatalogFilter(
|
data class SourcesCatalogFilter(
|
||||||
val types: Set<ContentType>,
|
val types: Set<ContentType>,
|
||||||
val locale: String?,
|
val locale: String?,
|
||||||
|
val isNewOnly: Boolean,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,14 +4,9 @@ import android.app.Activity
|
|||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuInflater
|
import android.view.MenuInflater
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
|
||||||
import androidx.appcompat.widget.PopupMenu
|
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
import androidx.core.view.MenuProvider
|
import androidx.core.view.MenuProvider
|
||||||
import org.koitharu.kotatsu.R
|
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
|
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
||||||
|
|
||||||
class SourcesCatalogMenuProvider(
|
class SourcesCatalogMenuProvider(
|
||||||
@@ -32,14 +27,7 @@ class SourcesCatalogMenuProvider(
|
|||||||
searchView.queryHint = searchMenuItem.title
|
searchView.queryHint = searchMenuItem.title
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
|
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = false
|
||||||
R.id.action_locales -> {
|
|
||||||
showLocalesMenu()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||||
(activity as? AppBarOwner)?.appBar?.setExpanded(false, true)
|
(activity as? AppBarOwner)?.appBar?.setExpanded(false, true)
|
||||||
@@ -57,24 +45,4 @@ class SourcesCatalogMenuProvider(
|
|||||||
viewModel.performSearch(newText?.trim().orEmpty())
|
viewModel.performSearch(newText?.trim().orEmpty())
|
||||||
return true
|
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,16 +7,16 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
import org.koitharu.kotatsu.R
|
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.BaseViewModel
|
||||||
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
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.MutableEventFlow
|
||||||
import org.koitharu.kotatsu.core.util.ext.call
|
import org.koitharu.kotatsu.core.util.ext.call
|
||||||
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
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.ContentType
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||||
@@ -27,6 +27,7 @@ import javax.inject.Inject
|
|||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class SourcesCatalogViewModel @Inject constructor(
|
class SourcesCatalogViewModel @Inject constructor(
|
||||||
private val repository: MangaSourcesRepository,
|
private val repository: MangaSourcesRepository,
|
||||||
|
private val settings: AppSettings,
|
||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
val onActionDone = MutableEventFlow<ReversibleAction>()
|
val onActionDone = MutableEventFlow<ReversibleAction>()
|
||||||
@@ -37,16 +38,14 @@ class SourcesCatalogViewModel @Inject constructor(
|
|||||||
SourcesCatalogFilter(
|
SourcesCatalogFilter(
|
||||||
types = emptySet(),
|
types = emptySet(),
|
||||||
locale = Locale.getDefault().language.takeIf { it in locales },
|
locale = Locale.getDefault().language.takeIf { it in locales },
|
||||||
|
isNewOnly = false,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
val hasNewSources = repository.observeNewSources()
|
val isNsfwDisabled = settings.isNsfwContentDisabled
|
||||||
.map { it.isNotEmpty() }
|
|
||||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, false)
|
|
||||||
|
|
||||||
val filter: StateFlow<List<ChipModel>> = appliedFilter.map {
|
val hasNewSources = repository.observeHasNewSources()
|
||||||
buildFilter(it)
|
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, false)
|
||||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, buildFilter(appliedFilter.value))
|
|
||||||
|
|
||||||
val content: StateFlow<List<SourceCatalogItem>> = combine(
|
val content: StateFlow<List<SourceCatalogItem>> = combine(
|
||||||
searchQuery,
|
searchQuery,
|
||||||
@@ -55,6 +54,10 @@ class SourcesCatalogViewModel @Inject constructor(
|
|||||||
buildSourcesList(f, q)
|
buildSourcesList(f, q)
|
||||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
|
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
|
||||||
|
|
||||||
|
init {
|
||||||
|
repository.clearNewSourcesBadge()
|
||||||
|
}
|
||||||
|
|
||||||
fun performSearch(query: String?) {
|
fun performSearch(query: String?) {
|
||||||
searchQuery.value = query?.trim()
|
searchQuery.value = query?.trim()
|
||||||
}
|
}
|
||||||
@@ -70,12 +73,6 @@ class SourcesCatalogViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun skipNewSources() {
|
|
||||||
launchJob {
|
|
||||||
repository.assimilateNewSources()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setContentType(value: ContentType, isAdd: Boolean) {
|
fun setContentType(value: ContentType, isAdd: Boolean) {
|
||||||
val filter = appliedFilter.value
|
val filter = appliedFilter.value
|
||||||
val types = EnumSet.noneOf(ContentType::class.java)
|
val types = EnumSet.noneOf(ContentType::class.java)
|
||||||
@@ -88,29 +85,19 @@ class SourcesCatalogViewModel @Inject constructor(
|
|||||||
appliedFilter.value = filter.copy(types = types)
|
appliedFilter.value = filter.copy(types = types)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildFilter(applied: SourcesCatalogFilter): List<ChipModel> = buildList(ContentType.entries.size) {
|
fun setNewOnly(value: Boolean) {
|
||||||
for (ct in ContentType.entries) {
|
appliedFilter.value = appliedFilter.value.copy(isNewOnly = value)
|
||||||
add(
|
|
||||||
ChipModel(
|
|
||||||
tint = 0,
|
|
||||||
title = ct.name,
|
|
||||||
icon = 0,
|
|
||||||
isCheckable = true,
|
|
||||||
isChecked = ct in applied.types,
|
|
||||||
data = ct,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun buildSourcesList(filter: SourcesCatalogFilter, query: String?): List<SourceCatalogItem> {
|
private suspend fun buildSourcesList(filter: SourcesCatalogFilter, query: String?): List<SourceCatalogItem> {
|
||||||
val sources = repository.getDisabledSources().toMutableList()
|
val sources = repository.getAvailableSources(
|
||||||
sources.retainAll {
|
isDisabledOnly = true,
|
||||||
(filter.types.isEmpty() || it.contentType in filter.types) && it.locale == filter.locale
|
isNewOnly = filter.isNewOnly,
|
||||||
}
|
excludeBroken = false,
|
||||||
if (!query.isNullOrEmpty()) {
|
types = filter.types,
|
||||||
sources.retainAll { it.title.contains(query, ignoreCase = true) }
|
query = query,
|
||||||
}
|
sortOrder = SourcesSortOrder.ALPHABETIC,
|
||||||
|
).filter { it.locale == filter.locale }
|
||||||
return if (sources.isEmpty()) {
|
return if (sources.isEmpty()) {
|
||||||
listOf(
|
listOf(
|
||||||
if (query == null) {
|
if (query == null) {
|
||||||
@@ -128,12 +115,8 @@ class SourcesCatalogViewModel @Inject constructor(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
sources.sortBy { it.title }
|
|
||||||
sources.map {
|
sources.map {
|
||||||
SourceCatalogItem.Source(
|
SourceCatalogItem.Source(source = it)
|
||||||
source = it,
|
|
||||||
showSummary = query != null,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="?android:windowBackground"
|
android:background="?selectableItemBackground"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:minHeight="?listPreferredItemHeightSmall"
|
android:minHeight="?listPreferredItemHeightSmall"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
@@ -52,10 +52,17 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="1dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginVertical="4dp"
|
||||||
|
android:background="?colorOutline" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/imageView_add"
|
android:id="@+id/imageView_add"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/list_spacing_small"
|
||||||
android:background="?selectableItemBackgroundBorderless"
|
android:background="?selectableItemBackgroundBorderless"
|
||||||
android:contentDescription="@string/add"
|
android:contentDescription="@string/add"
|
||||||
android:padding="@dimen/margin_small"
|
android:padding="@dimen/margin_small"
|
||||||
|
|||||||
@@ -3,12 +3,6 @@
|
|||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/action_locales"
|
|
||||||
android:icon="@drawable/ic_expand_more"
|
|
||||||
android:title="@string/languages"
|
|
||||||
app:showAsAction="ifRoom" />
|
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_search"
|
android:id="@+id/action_search"
|
||||||
android:icon="?actionModeWebSearchDrawable"
|
android:icon="?actionModeWebSearchDrawable"
|
||||||
|
|||||||
@@ -650,4 +650,6 @@
|
|||||||
<string name="disable_nsfw_notifications_summary">Do not show notifications about NSFW manga updates</string>
|
<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">Checking for new chapters log</string>
|
||||||
<string name="tracker_debug_info_summary">Debug information about background checks for new chapters</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>
|
</resources>
|
||||||
|
|||||||
@@ -31,10 +31,4 @@
|
|||||||
android:summary="@string/disable_nsfw_summary"
|
android:summary="@string/disable_nsfw_summary"
|
||||||
android:title="@string/disable_nsfw" />
|
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>
|
</androidx.preference.PreferenceScreen>
|
||||||
|
|||||||
Reference in New Issue
Block a user