Configurable manga lists badges

master
Koitharu 1 year ago
parent 4148f4a4b9
commit 4ee52e149e
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -16,6 +16,7 @@ import androidx.documentfile.provider.DocumentFile
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import org.json.JSONArray import org.json.JSONArray
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.ZoomMode import org.koitharu.kotatsu.core.model.ZoomMode
import org.koitharu.kotatsu.core.network.DoHProvider import org.koitharu.kotatsu.core.network.DoHProvider
import org.koitharu.kotatsu.core.util.ext.connectivityManager import org.koitharu.kotatsu.core.util.ext.connectivityManager
@ -44,6 +45,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
private val prefs = PreferenceManager.getDefaultSharedPreferences(context) private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
private val connectivityManager = context.connectivityManager private val connectivityManager = context.connectivityManager
private val mangaListBadgesDefault = ArraySet(context.resources.getStringArray(R.array.values_list_badges))
var listMode: ListMode var listMode: ListMode
get() = prefs.getEnumValue(KEY_LIST_MODE, ListMode.GRID) get() = prefs.getEnumValue(KEY_LIST_MODE, ListMode.GRID)
@ -546,6 +548,15 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
prefs.edit { putString(KEY_PAGES_SAVE_DIR, uri?.toString()) } prefs.edit { putString(KEY_PAGES_SAVE_DIR, uri?.toString()) }
} }
fun getMangaListBadges(): Int {
val raw = prefs.getStringSet(KEY_MANGA_LIST_BADGES, mangaListBadgesDefault).orEmpty()
var result = 0
for (item in raw) {
result = result or item.toInt()
}
return result
}
fun subscribe(listener: SharedPreferences.OnSharedPreferenceChangeListener) { fun subscribe(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
prefs.registerOnSharedPreferenceChangeListener(listener) prefs.registerOnSharedPreferenceChangeListener(listener)
} }
@ -735,12 +746,12 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_QUICK_FILTER = "quick_filter" const val KEY_QUICK_FILTER = "quick_filter"
const val KEY_BACKUP_TG_ENABLED = "backup_periodic_tg_enabled" const val KEY_BACKUP_TG_ENABLED = "backup_periodic_tg_enabled"
const val KEY_BACKUP_TG_CHAT = "backup_periodic_tg_chat_id" const val KEY_BACKUP_TG_CHAT = "backup_periodic_tg_chat_id"
const val KEY_MANGA_LIST_BADGES = "manga_list_badges"
// keys for non-persistent preferences // keys for non-persistent preferences
const val KEY_APP_VERSION = "app_version" const val KEY_APP_VERSION = "app_version"
const val KEY_IGNORE_DOZE = "ignore_dose" const val KEY_IGNORE_DOZE = "ignore_dose"
const val KEY_TRACKER_DEBUG = "tracker_debug" const val KEY_TRACKER_DEBUG = "tracker_debug"
const val KEY_APP_UPDATE = "app_update"
const val KEY_LINK_WEBLATE = "about_app_translation" const val KEY_LINK_WEBLATE = "about_app_translation"
const val KEY_LINK_TELEGRAM = "about_telegram" const val KEY_LINK_TELEGRAM = "about_telegram"
const val KEY_LINK_GITHUB = "about_github" const val KEY_LINK_GITHUB = "about_github"

@ -11,7 +11,7 @@ fun ListPreference.setDefaultValueCompat(defaultValue: String) {
} }
fun MultiSelectListPreference.setDefaultValueCompat(defaultValue: Set<String>) { fun MultiSelectListPreference.setDefaultValueCompat(defaultValue: Set<String>) {
setDefaultValue(defaultValue) setDefaultValue(defaultValue) // FIXME not working
} }
fun <E : Enum<E>> SharedPreferences.getEnumValue(key: String, enumClass: Class<E>): E? { fun <E : Enum<E>> SharedPreferences.getEnumValue(key: String, enumClass: Class<E>): E? {

@ -146,7 +146,6 @@ class DetailsViewModel @Inject constructor(
mangaListMapper.toListModelList( mangaListMapper.toListModelList(
manga = relatedMangaUseCase(it).orEmpty(), manga = relatedMangaUseCase(it).orEmpty(),
mode = ListMode.GRID, mode = ListMode.GRID,
flags = 0,
) )
} else { } else {
emptyList() emptyList()

@ -52,7 +52,7 @@ class RelatedListViewModel @Inject constructor(
list.isNullOrEmpty() && error != null -> listOf(error.toErrorState(canRetry = true)) list.isNullOrEmpty() && error != null -> listOf(error.toErrorState(canRetry = true))
list == null -> listOf(LoadingState) list == null -> listOf(LoadingState)
list.isEmpty() -> listOf(createEmptyState()) list.isEmpty() -> listOf(createEmptyState())
else -> mangaListMapper.toListModelList(list, mode, 0) else -> mangaListMapper.toListModelList(list, mode)
} }
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState)) }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))

@ -204,9 +204,6 @@ class ExploreViewModel @Inject constructor(
coverUrl = manga.coverUrl, coverUrl = manga.coverUrl,
manga = manga, manga = manga,
counter = 0, counter = 0,
progress = null,
isFavorite = false,
isSaved = false,
) )
} }

@ -190,7 +190,7 @@ class HistoryListViewModel @Inject constructor(
prevHeader = header prevHeader = header
} }
} }
result += mangaListMapper.toListModel(manga, mode, 0) result += mangaListMapper.toListModel(manga, mode)
} }
if (filters.isNotEmpty() && isEmpty) { if (filters.isNotEmpty() && isEmpty) {
result += getEmptyState(hasFilters = true) result += getEmptyState(hasFilters = true)

@ -1,5 +1,6 @@
package org.koitharu.kotatsu.list.domain package org.koitharu.kotatsu.list.domain
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import androidx.annotation.ColorRes import androidx.annotation.ColorRes
import androidx.annotation.IntDef import androidx.annotation.IntDef
@ -38,77 +39,82 @@ class MangaListMapper @Inject constructor(
suspend fun toListModelList( suspend fun toListModelList(
manga: Collection<Manga>, manga: Collection<Manga>,
mode: ListMode, mode: ListMode,
@Flags flags: Int @Flags flags: Int = DEFAULTS,
): List<MangaListModel> = manga.map { ): List<MangaListModel> {
toListModel(it, mode, flags) val options = getOptions(flags)
return manga.map { toListModelImpl(it, mode, options) }
} }
suspend fun toListModelList( suspend fun toListModelList(
destination: MutableCollection<in MangaListModel>, destination: MutableCollection<in MangaListModel>,
manga: Collection<Manga>, manga: Collection<Manga>,
mode: ListMode, mode: ListMode,
@Flags flags: Int, @Flags flags: Int = DEFAULTS,
) { ) {
val options = getOptions(flags)
manga.mapTo(destination) { manga.mapTo(destination) {
toListModel(it, mode, flags) toListModelImpl(it, mode, options)
} }
} }
suspend fun toListModel( suspend fun toListModel(
manga: Manga, manga: Manga,
mode: ListMode, mode: ListMode,
@Flags flags: Int @Flags flags: Int = DEFAULTS,
): MangaListModel = when (mode) { ): MangaListModel = toListModelImpl(manga, mode, getOptions(flags))
ListMode.LIST -> toCompactListModel(manga, flags)
ListMode.DETAILED_LIST -> toDetailedListModel(manga, flags) fun mapTags(tags: Collection<MangaTag>) = tags.map {
ListMode.GRID -> toGridModel(manga, flags) ChipsView.ChipModel(
tint = getTagTint(it),
title = it.title,
data = it,
)
} }
suspend fun toCompactListModel(manga: Manga, @Flags flags: Int) = MangaCompactListModel( private suspend fun toCompactListModel(manga: Manga, @Options options: Int) = MangaCompactListModel(
id = manga.id, id = manga.id,
title = manga.title, title = manga.title,
subtitle = manga.tags.joinToString(", ") { it.title }, subtitle = manga.tags.joinToString(", ") { it.title },
coverUrl = manga.coverUrl, coverUrl = manga.coverUrl,
manga = manga, manga = manga,
counter = getCounter(manga.id, flags), counter = getCounter(manga.id, options),
progress = getProgress(manga.id, flags),
isFavorite = isFavorite(manga.id, flags),
isSaved = isSaved(manga.id, flags),
) )
suspend fun toDetailedListModel(manga: Manga, @Flags flags: Int) = MangaDetailedListModel( private suspend fun toDetailedListModel(manga: Manga, @Options options: Int) = MangaDetailedListModel(
id = manga.id, id = manga.id,
title = manga.title, title = manga.title,
subtitle = manga.altTitle, subtitle = manga.altTitle,
coverUrl = manga.coverUrl, coverUrl = manga.coverUrl,
manga = manga, manga = manga,
counter = getCounter(manga.id, flags), counter = getCounter(manga.id, options),
progress = getProgress(manga.id, flags), progress = getProgress(manga.id, options),
isFavorite = isFavorite(manga.id, flags), isFavorite = isFavorite(manga.id, options),
isSaved = isSaved(manga.id, flags), isSaved = isSaved(manga.id, options),
tags = mapTags(manga.tags), tags = mapTags(manga.tags),
) )
suspend fun toGridModel(manga: Manga, @Flags flags: Int) = MangaGridModel( private suspend fun toGridModel(manga: Manga, @Options options: Int) = MangaGridModel(
id = manga.id, id = manga.id,
title = manga.title, title = manga.title,
coverUrl = manga.coverUrl, coverUrl = manga.coverUrl,
manga = manga, manga = manga,
counter = getCounter(manga.id, flags), counter = getCounter(manga.id, options),
progress = getProgress(manga.id, flags), progress = getProgress(manga.id, options),
isFavorite = isFavorite(manga.id, flags), isFavorite = isFavorite(manga.id, options),
isSaved = isSaved(manga.id, flags), isSaved = isSaved(manga.id, options),
) )
fun mapTags(tags: Collection<MangaTag>) = tags.map { private suspend fun toListModelImpl(
ChipsView.ChipModel( manga: Manga,
tint = getTagTint(it), mode: ListMode,
title = it.title, @Options options: Int
data = it, ): MangaListModel = when (mode) {
) ListMode.LIST -> toCompactListModel(manga, options)
ListMode.DETAILED_LIST -> toDetailedListModel(manga, options)
ListMode.GRID -> toGridModel(manga, options)
} }
private suspend fun getCounter(mangaId: Long, @Flags flags: Int): Int { private suspend fun getCounter(mangaId: Long, @Options options: Int): Int {
return if (settings.isTrackerEnabled) { return if (settings.isTrackerEnabled) {
trackingRepository.getNewChaptersCount(mangaId) trackingRepository.getNewChaptersCount(mangaId)
} else { } else {
@ -116,20 +122,20 @@ class MangaListMapper @Inject constructor(
} }
} }
private suspend fun getProgress(mangaId: Long, @Flags flags: Int): ReadingProgress? { private suspend fun getProgress(mangaId: Long, @Options options: Int): ReadingProgress? {
return if (flags.hasNoFlag(NO_PROGRESS)) { return if (options.isBadgeEnabled(PROGRESS)) {
historyRepository.getProgress(mangaId, settings.progressIndicatorMode) historyRepository.getProgress(mangaId, settings.progressIndicatorMode)
} else { } else {
null null
} }
} }
private suspend fun isFavorite(mangaId: Long, @Flags flags: Int): Boolean { private suspend fun isFavorite(mangaId: Long, @Options options: Int): Boolean {
return flags.hasNoFlag(NO_FAVORITE) && favouritesRepository.isFavorite(mangaId) return options.isBadgeEnabled(FAVORITE) && favouritesRepository.isFavorite(mangaId)
} }
private suspend fun isSaved(mangaId: Long, @Flags flags: Int): Boolean { private suspend fun isSaved(mangaId: Long, @Options options: Int): Boolean {
return flags.hasNoFlag(NO_SAVED) && mangaId in localMangaIndex return options.isBadgeEnabled(SAVED) && mangaId in localMangaIndex
} }
@ColorRes @ColorRes
@ -154,17 +160,34 @@ class MangaListMapper @Inject constructor(
set set
} }
private fun Int.hasNoFlag(flag: Int) = this and flag == 0 private fun Int.isBadgeEnabled(@Options badge: Int) = this and badge == badge
@Options
@SuppressLint("WrongConstant")
private fun getOptions(@Flags flags: Int): Int {
var options = settings.getMangaListBadges() or PROGRESS
options = options and flags.inv()
return options
}
@IntDef(0, NO_SAVED, NO_PROGRESS, NO_FAVORITE) @IntDef(DEFAULTS, NO_SAVED, NO_PROGRESS, NO_FAVORITE, flag = true)
@Retention(AnnotationRetention.SOURCE) @Retention(AnnotationRetention.SOURCE)
annotation class Flags annotation class Flags
@IntDef(NONE, SAVED, FAVORITE, PROGRESS)
@Retention(AnnotationRetention.SOURCE)
private annotation class Options
companion object { companion object {
const val NO_SAVED = 1 private const val NONE = 0
const val NO_PROGRESS = 2 private const val SAVED = 1
const val NO_FAVORITE = 4 private const val PROGRESS = 2
private const val FAVORITE = 4
const val DEFAULTS = NONE
const val NO_SAVED = SAVED
const val NO_PROGRESS = PROGRESS
const val NO_FAVORITE = FAVORITE
} }
} }

@ -66,6 +66,7 @@ abstract class MangaListViewModel(
key == AppSettings.KEY_PROGRESS_INDICATORS key == AppSettings.KEY_PROGRESS_INDICATORS
|| key == AppSettings.KEY_TRACKER_ENABLED || key == AppSettings.KEY_TRACKER_ENABLED
|| key == AppSettings.KEY_QUICK_FILTER || key == AppSettings.KEY_QUICK_FILTER
|| key == AppSettings.KEY_MANGA_LIST_BADGES
}.onStart { emit("") }, }.onStart { emit("") },
) { mode, _ -> ) { mode, _ ->
mode mode

@ -6,6 +6,7 @@ import coil3.ImageLoader
import coil3.request.allowRgb565 import coil3.request.allowRgb565
import coil3.request.transformations import coil3.request.transformations
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
import org.koitharu.kotatsu.core.ui.image.TrimTransformation import org.koitharu.kotatsu.core.ui.image.TrimTransformation
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
@ -36,6 +37,12 @@ fun mangaListDetailedItemAD(
value = item.progress, value = item.progress,
animate = ListModelDiffCallback.PAYLOAD_PROGRESS_CHANGED in payloads, animate = ListModelDiffCallback.PAYLOAD_PROGRESS_CHANGED in payloads,
) )
with(binding.iconsView) {
clearIcons()
if (item.isSaved) addIcon(R.drawable.ic_storage)
if (item.isFavorite) addIcon(R.drawable.ic_heart_outline)
isVisible = iconsCount > 0
}
binding.imageViewCover.newImageRequest(lifecycleOwner, item.coverUrl)?.run { binding.imageViewCover.newImageRequest(lifecycleOwner, item.coverUrl)?.run {
size(CoverSizeResolver(binding.imageViewCover)) size(CoverSizeResolver(binding.imageViewCover))
defaultPlaceholders(context) defaultPlaceholders(context)

@ -1,6 +1,5 @@
package org.koitharu.kotatsu.list.ui.model package org.koitharu.kotatsu.list.ui.model
import org.koitharu.kotatsu.list.domain.ReadingProgress
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
data class MangaCompactListModel( data class MangaCompactListModel(
@ -10,7 +9,4 @@ data class MangaCompactListModel(
override val coverUrl: String?, override val coverUrl: String?,
override val manga: Manga, override val manga: Manga,
override val counter: Int, override val counter: Int,
override val progress: ReadingProgress?,
override val isFavorite: Boolean,
override val isSaved: Boolean,
) : MangaListModel() ) : MangaListModel()

@ -2,6 +2,8 @@ package org.koitharu.kotatsu.list.ui.model
import org.koitharu.kotatsu.core.ui.widgets.ChipsView import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.list.domain.ReadingProgress import org.koitharu.kotatsu.list.domain.ReadingProgress
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback.Companion.PAYLOAD_ANYTHING_CHANGED
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback.Companion.PAYLOAD_PROGRESS_CHANGED
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
data class MangaDetailedListModel( data class MangaDetailedListModel(
@ -11,8 +13,19 @@ data class MangaDetailedListModel(
override val coverUrl: String?, override val coverUrl: String?,
override val manga: Manga, override val manga: Manga,
override val counter: Int, override val counter: Int,
override val progress: ReadingProgress?, val progress: ReadingProgress?,
override val isFavorite: Boolean, val isFavorite: Boolean,
override val isSaved: Boolean, val isSaved: Boolean,
val tags: List<ChipsView.ChipModel>, val tags: List<ChipsView.ChipModel>,
) : MangaListModel() ) : MangaListModel() {
override fun getChangePayload(previousState: ListModel): Any? = when {
previousState !is MangaDetailedListModel || previousState.manga != manga -> null
previousState.progress != progress -> PAYLOAD_PROGRESS_CHANGED
previousState.isFavorite != isFavorite ||
previousState.isSaved != isSaved -> PAYLOAD_ANYTHING_CHANGED
else -> super.getChangePayload(previousState)
}
}

@ -1,6 +1,8 @@
package org.koitharu.kotatsu.list.ui.model package org.koitharu.kotatsu.list.ui.model
import org.koitharu.kotatsu.list.domain.ReadingProgress import org.koitharu.kotatsu.list.domain.ReadingProgress
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback.Companion.PAYLOAD_ANYTHING_CHANGED
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback.Companion.PAYLOAD_PROGRESS_CHANGED
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
data class MangaGridModel( data class MangaGridModel(
@ -9,7 +11,18 @@ data class MangaGridModel(
override val coverUrl: String?, override val coverUrl: String?,
override val manga: Manga, override val manga: Manga,
override val counter: Int, override val counter: Int,
override val progress: ReadingProgress?, val progress: ReadingProgress?,
override val isFavorite: Boolean, val isFavorite: Boolean,
override val isSaved: Boolean, val isSaved: Boolean,
) : MangaListModel() ) : MangaListModel() {
override fun getChangePayload(previousState: ListModel): Any? = when {
previousState !is MangaGridModel || previousState.manga != manga -> null
previousState.progress != progress -> PAYLOAD_PROGRESS_CHANGED
previousState.isFavorite != isFavorite ||
previousState.isSaved != isSaved -> PAYLOAD_ANYTHING_CHANGED
else -> super.getChangePayload(previousState)
}
}

@ -1,8 +1,6 @@
package org.koitharu.kotatsu.list.ui.model package org.koitharu.kotatsu.list.ui.model
import org.koitharu.kotatsu.list.domain.ReadingProgress
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback.Companion.PAYLOAD_ANYTHING_CHANGED import org.koitharu.kotatsu.list.ui.ListModelDiffCallback.Companion.PAYLOAD_ANYTHING_CHANGED
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback.Companion.PAYLOAD_PROGRESS_CHANGED
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
@ -13,9 +11,6 @@ sealed class MangaListModel : ListModel {
abstract val title: String abstract val title: String
abstract val coverUrl: String? abstract val coverUrl: String?
abstract val counter: Int abstract val counter: Int
abstract val isFavorite: Boolean
abstract val isSaved: Boolean
abstract val progress: ReadingProgress?
val source: MangaSource val source: MangaSource
get() = manga.source get() = manga.source
@ -26,12 +21,7 @@ sealed class MangaListModel : ListModel {
override fun getChangePayload(previousState: ListModel): Any? = when { override fun getChangePayload(previousState: ListModel): Any? = when {
previousState !is MangaListModel || previousState.manga != manga -> null previousState !is MangaListModel || previousState.manga != manga -> null
previousState.progress != progress -> PAYLOAD_PROGRESS_CHANGED
previousState.isFavorite != isFavorite ||
previousState.isSaved != isSaved ||
previousState.counter != counter -> PAYLOAD_ANYTHING_CHANGED previousState.counter != counter -> PAYLOAD_ANYTHING_CHANGED
else -> null else -> null
} }
} }

@ -37,7 +37,6 @@ import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingFooter import org.koitharu.kotatsu.list.ui.model.LoadingFooter
import org.koitharu.kotatsu.list.ui.model.LoadingState import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.list.ui.model.MangaListModel
import org.koitharu.kotatsu.list.ui.model.toErrorFooter import org.koitharu.kotatsu.list.ui.model.toErrorFooter
import org.koitharu.kotatsu.list.ui.model.toErrorState import org.koitharu.kotatsu.list.ui.model.toErrorState
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
@ -177,7 +176,7 @@ open class RemoteListViewModel @Inject constructor(
destination: MutableCollection<in ListModel>, destination: MutableCollection<in ListModel>,
manga: Collection<Manga>, manga: Collection<Manga>,
mode: ListMode mode: ListMode
) = mangaListMapper.toListModelList(destination, manga, mode, 0) ) = mangaListMapper.toListModelList(destination, manga, mode)
fun openRandom() { fun openRandom() {
if (randomJob?.isActive == true) { if (randomJob?.isActive == true) {

@ -126,7 +126,6 @@ class SearchViewModel @Inject constructor(
mangaListMapper.toListModelList( mangaListMapper.toListModelList(
manga = repository.getList(offset = 0, null, MangaListFilter(query = q)), manga = repository.getList(offset = 0, null, MangaListFilter(query = q)),
mode = ListMode.GRID, mode = ListMode.GRID,
flags = 0,
) )
} }
}.fold( }.fold(
@ -162,7 +161,7 @@ class SearchViewModel @Inject constructor(
titleResId = R.string.history, titleResId = R.string.history,
source = UnknownMangaSource, source = UnknownMangaSource,
hasMore = false, hasMore = false,
list = mangaListMapper.toListModelList(manga = result, mode = ListMode.GRID, flags = 0), list = mangaListMapper.toListModelList(manga = result, mode = ListMode.GRID),
error = null, error = null,
) )
} else { } else {
@ -191,7 +190,7 @@ class SearchViewModel @Inject constructor(
titleResId = R.string.favourites, titleResId = R.string.favourites,
source = UnknownMangaSource, source = UnknownMangaSource,
hasMore = false, hasMore = false,
list = mangaListMapper.toListModelList(manga = result, mode = ListMode.GRID, flags = 0), list = mangaListMapper.toListModelList(manga = result, mode = ListMode.GRID),
error = null, error = null,
) )
} else { } else {
@ -220,7 +219,7 @@ class SearchViewModel @Inject constructor(
titleResId = 0, titleResId = 0,
source = LocalMangaSource, source = LocalMangaSource,
hasMore = result.size > MIN_HAS_MORE_ITEMS, hasMore = result.size > MIN_HAS_MORE_ITEMS,
list = mangaListMapper.toListModelList(manga = result, mode = ListMode.GRID,flags = 0), list = mangaListMapper.toListModelList(manga = result, mode = ListMode.GRID),
error = null, error = null,
) )
} else { } else {

@ -9,6 +9,7 @@ import android.provider.Settings
import android.view.View import android.view.View
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.Preference import androidx.preference.Preference
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
@ -26,6 +27,7 @@ import org.koitharu.kotatsu.core.util.ext.toList
import org.koitharu.kotatsu.parsers.util.names import org.koitharu.kotatsu.parsers.util.names
import org.koitharu.kotatsu.parsers.util.toTitleCase import org.koitharu.kotatsu.parsers.util.toTitleCase
import org.koitharu.kotatsu.settings.utils.ActivityListPreference import org.koitharu.kotatsu.settings.utils.ActivityListPreference
import org.koitharu.kotatsu.settings.utils.MultiSummaryProvider
import org.koitharu.kotatsu.settings.utils.PercentSummaryProvider import org.koitharu.kotatsu.settings.utils.PercentSummaryProvider
import org.koitharu.kotatsu.settings.utils.SliderPreference import org.koitharu.kotatsu.settings.utils.SliderPreference
import javax.inject.Inject import javax.inject.Inject
@ -63,6 +65,9 @@ class AppearanceSettingsFragment :
} }
setDefaultValueCompat("") setDefaultValueCompat("")
} }
findPreference<MultiSelectListPreference>(AppSettings.KEY_MANGA_LIST_BADGES)?.run {
summaryProvider = MultiSummaryProvider(R.string.none)
}
bindNavSummary() bindNavSummary()
} }

@ -67,7 +67,7 @@ class SuggestionsViewModel @Inject constructor(
else -> buildList(list.size + 1) { else -> buildList(list.size + 1) {
quickFilter.filterItem(filters)?.let(::add) quickFilter.filterItem(filters)?.let(::add)
mangaListMapper.toListModelList(this, list, mode, 0) mangaListMapper.toListModelList(this, list, mode)
} }
} }
}.onStart { }.onStart {

@ -1,5 +1,6 @@
package org.koitharu.kotatsu.tracker.data package org.koitharu.kotatsu.tracker.data
import androidx.annotation.IntDef
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.ForeignKey import androidx.room.ForeignKey
@ -24,10 +25,15 @@ class TrackEntity(
@ColumnInfo(name = "chapters_new") val newChapters: Int, @ColumnInfo(name = "chapters_new") val newChapters: Int,
@ColumnInfo(name = "last_check_time") val lastCheckTime: Long, @ColumnInfo(name = "last_check_time") val lastCheckTime: Long,
@ColumnInfo(name = "last_chapter_date") val lastChapterDate: Long, @ColumnInfo(name = "last_chapter_date") val lastChapterDate: Long,
@TrackerResult
@ColumnInfo(name = "last_result") val lastResult: Int, @ColumnInfo(name = "last_result") val lastResult: Int,
@ColumnInfo(name = "last_error") val lastError: String?, @ColumnInfo(name = "last_error") val lastError: String?,
) { ) {
@IntDef(RESULT_NONE, RESULT_HAS_UPDATE, RESULT_NO_UPDATE, RESULT_FAILED, RESULT_EXTERNAL_MODIFICATION)
@Retention(AnnotationRetention.SOURCE)
annotation class TrackerResult
companion object { companion object {
const val RESULT_NONE = 0 const val RESULT_NONE = 0

@ -11,7 +11,6 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import coil3.ImageLoader import coil3.ImageLoader
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.drop
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
@ -21,6 +20,7 @@ import org.koitharu.kotatsu.core.ui.BaseFragment
import org.koitharu.kotatsu.core.ui.list.PaginationScrollListener import org.koitharu.kotatsu.core.ui.list.PaginationScrollListener
import org.koitharu.kotatsu.core.ui.list.RecyclerScrollKeeper import org.koitharu.kotatsu.core.ui.list.RecyclerScrollKeeper
import org.koitharu.kotatsu.core.ui.util.MenuInvalidator import org.koitharu.kotatsu.core.ui.util.MenuInvalidator
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
import org.koitharu.kotatsu.core.ui.widgets.TipView 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.observe import org.koitharu.kotatsu.core.util.ext.observe
@ -31,7 +31,6 @@ import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
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.size.StaticItemSizeResolver import org.koitharu.kotatsu.list.ui.size.StaticItemSizeResolver
import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.tracker.ui.feed.adapter.FeedAdapter import org.koitharu.kotatsu.tracker.ui.feed.adapter.FeedAdapter
@ -76,7 +75,7 @@ class FeedFragment :
viewModel.isHeaderEnabled.drop(1).observe(viewLifecycleOwner, MenuInvalidator(requireActivity())) viewModel.isHeaderEnabled.drop(1).observe(viewLifecycleOwner, MenuInvalidator(requireActivity()))
viewModel.content.observe(viewLifecycleOwner, feedAdapter) viewModel.content.observe(viewLifecycleOwner, feedAdapter)
viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this)) viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this))
viewModel.onFeedCleared.observeEvent(viewLifecycleOwner) { onFeedCleared() } viewModel.onActionDone.observeEvent(viewLifecycleOwner, ReversibleActionObserver(binding.recyclerView))
viewModel.isRunning.observe(viewLifecycleOwner, this::onIsTrackerRunningChanged) viewModel.isRunning.observe(viewLifecycleOwner, this::onIsTrackerRunningChanged)
} }
@ -107,16 +106,6 @@ class FeedFragment :
router.openMangaUpdates() router.openMangaUpdates()
} }
private fun onFeedCleared() {
val snackbar = Snackbar.make(
requireViewBinding().recyclerView,
R.string.updates_feed_cleared,
Snackbar.LENGTH_LONG,
)
snackbar.anchorView = (activity as? BottomNavOwner)?.bottomNav
snackbar.show()
}
private fun onIsTrackerRunningChanged(isRunning: Boolean) { private fun onIsTrackerRunningChanged(isRunning: Boolean) {
requireViewBinding().swipeRefreshLayout.isRefreshing = isRunning requireViewBinding().swipeRefreshLayout.isRefreshing = isRunning
} }

@ -16,10 +16,12 @@ 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.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.core.prefs.observeAsFlow import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.ui.model.DateTimeAgo import org.koitharu.kotatsu.core.ui.model.DateTimeAgo
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.calculateTimeAgo import org.koitharu.kotatsu.core.util.ext.calculateTimeAgo
import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.core.util.ext.call
@ -64,7 +66,7 @@ class FeedViewModel @Inject constructor(
valueProducer = { isFeedHeaderVisible }, valueProducer = { isFeedHeaderVisible },
) )
val onFeedCleared = MutableEventFlow<Unit>() val onActionDone = MutableEventFlow<ReversibleAction>()
@Suppress("USELESS_CAST") @Suppress("USELESS_CAST")
val content = combine( val content = combine(
@ -106,7 +108,7 @@ class FeedViewModel @Inject constructor(
if (clearCounters) { if (clearCounters) {
repository.clearCounters() repository.clearCounters()
} }
onFeedCleared.call(Unit) onActionDone.call(ReversibleAction(R.string.updates_feed_cleared, null))
} }
} }
@ -151,7 +153,7 @@ class FeedViewModel @Inject constructor(
null null
} else { } else {
UpdatedMangaHeader( UpdatedMangaHeader(
mangaList.map { mangaListMapper.toGridModel(it.manga, 0) }, mangaList.map { mangaListMapper.toListModel(it.manga, ListMode.GRID) },
) )
} }
} }

@ -107,7 +107,7 @@ class UpdatesViewModel @Inject constructor(
prevHeader = header prevHeader = header
} }
} }
result += mangaListMapper.toListModel(item.manga, mode, 0) result += mangaListMapper.toListModel(item.manga, mode)
} }
return result return result
} }

@ -38,13 +38,13 @@
android:id="@+id/iconsView" android:id="@+id/iconsView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="bottom|start" android:layout_gravity="top|start"
android:layout_marginBottom="@dimen/card_indicator_offset" android:layout_marginTop="@dimen/card_indicator_offset"
android:background="@drawable/bg_list_icons" android:background="@drawable/bg_list_icons"
android:orientation="horizontal" android:orientation="horizontal"
android:padding="4dp" android:padding="4dp"
app:iconSize="12dp" app:iconSize="14dp"
app:iconSpacing="2dp" /> app:iconSpacing="4dp" />
<org.koitharu.kotatsu.core.ui.widgets.BadgeView <org.koitharu.kotatsu.core.ui.widgets.BadgeView
android:id="@+id/badge" android:id="@+id/badge"

@ -32,9 +32,22 @@
app:layout_constraintBottom_toBottomOf="@id/imageView_cover" app:layout_constraintBottom_toBottomOf="@id/imageView_cover"
app:layout_constraintEnd_toEndOf="@id/imageView_cover" /> app:layout_constraintEnd_toEndOf="@id/imageView_cover" />
<org.koitharu.kotatsu.core.ui.widgets.IconsView
android:id="@+id/iconsView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/card_indicator_offset"
android:background="@drawable/bg_list_icons"
android:orientation="horizontal"
android:padding="4dp"
app:iconSize="14dp"
app:iconSpacing="4dp"
app:layout_constraintStart_toStartOf="@id/imageView_cover"
app:layout_constraintTop_toTopOf="@id/imageView_cover" />
<LinearLayout <LinearLayout
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="0dp"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginEnd="12dp" android:layout_marginEnd="12dp"
android:orientation="vertical" android:orientation="vertical"

@ -132,4 +132,8 @@
<item>@string/screen_orientation</item> <item>@string/screen_orientation</item>
<item>@string/save_page</item> <item>@string/save_page</item>
</string-array> </string-array>
<string-array name="list_badges" translatable="false">
<item>@string/favourites</item>
<item>@string/saved_manga</item>
</string-array>
</resources> </resources>

@ -77,4 +77,9 @@
<item>1</item> <item>1</item>
<item>2</item> <item>2</item>
</string-array> </string-array>
<string-array name="values_list_badges" translatable="false">
<!-- MangaListMapper flags -->
<item>4</item>
<item>1</item>
</string-array>
</resources> </resources>

@ -800,4 +800,5 @@
<string name="pages_slider">Page switch slider</string> <string name="pages_slider">Page switch slider</string>
<string name="screen_rotation_locked">Screen rotation has been locked</string> <string name="screen_rotation_locked">Screen rotation has been locked</string>
<string name="screen_rotation_unlocked">Screen rotation has been unlocked</string> <string name="screen_rotation_unlocked">Screen rotation has been unlocked</string>
<string name="badges_in_lists">Badges in lists</string>
</resources> </resources>

@ -51,6 +51,13 @@
android:title="@string/show_reading_indicators" android:title="@string/show_reading_indicators"
app:useSimpleSummaryProvider="true" /> app:useSimpleSummaryProvider="true" />
<MultiSelectListPreference
android:defaultValue="@array/values_list_badges"
android:entries="@array/list_badges"
android:entryValues="@array/values_list_badges"
android:key="manga_list_badges"
android:title="@string/badges_in_lists" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory android:title="@string/details"> <PreferenceCategory android:title="@string/details">

Loading…
Cancel
Save