Saved and favorites indicators in manga lists (Draft implementation)(#1286)
This commit is contained in:
@@ -0,0 +1,90 @@
|
|||||||
|
package org.koitharu.kotatsu.core.ui.widgets
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.core.content.withStyledAttributes
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
|
||||||
|
class IconsView @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
) : LinearLayout(context, attrs) {
|
||||||
|
|
||||||
|
private var iconSize = LinearLayout.LayoutParams.WRAP_CONTENT
|
||||||
|
private var iconSpacing = 0
|
||||||
|
|
||||||
|
val iconsCount: Int
|
||||||
|
get() {
|
||||||
|
var count = 0
|
||||||
|
repeat(childCount) { i ->
|
||||||
|
if (getChildAt(i).isVisible) {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
context.withStyledAttributes(attrs, R.styleable.IconsView) {
|
||||||
|
iconSize = getDimensionPixelSize(R.styleable.IconsView_iconSize, iconSize)
|
||||||
|
iconSpacing = getDimensionPixelOffset(R.styleable.IconsView_iconSpacing, iconSpacing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setIcons(icons: Iterable<Drawable>) {
|
||||||
|
var index = 0
|
||||||
|
for (icon in icons) {
|
||||||
|
val imageView = (getChildAt(index) as ImageView?) ?: addImageView()
|
||||||
|
imageView.setImageDrawable(icon)
|
||||||
|
imageView.isVisible = true
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
for (i in index until childCount) {
|
||||||
|
val imageView = getChildAt(i) as? ImageView ?: continue
|
||||||
|
imageView.setImageDrawable(null)
|
||||||
|
imageView.isVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearIcons() {
|
||||||
|
repeat(childCount) { i ->
|
||||||
|
getChildAt(i).isVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addIcon(drawable: Drawable) {
|
||||||
|
val imageView = getNextImageView()
|
||||||
|
imageView.setImageDrawable(drawable)
|
||||||
|
imageView.isVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addIcon(@DrawableRes resId: Int) {
|
||||||
|
val imageView = getNextImageView()
|
||||||
|
imageView.setImageResource(resId)
|
||||||
|
imageView.isVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getNextImageView(): ImageView {
|
||||||
|
repeat(childCount) { i ->
|
||||||
|
val child = getChildAt(i)
|
||||||
|
if (child is ImageView && !child.isVisible) {
|
||||||
|
return child
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return addImageView()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addImageView() = ImageView(context).also {
|
||||||
|
it.scaleType = ImageView.ScaleType.FIT_CENTER
|
||||||
|
val lp = LayoutParams(iconSize, iconSize)
|
||||||
|
if (childCount != 0) {
|
||||||
|
lp.marginStart = iconSpacing
|
||||||
|
}
|
||||||
|
addView(it, lp)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -146,6 +146,7 @@ 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)
|
else -> mangaListMapper.toListModelList(list, mode, 0)
|
||||||
}
|
}
|
||||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
|
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
|
||||||
|
|
||||||
|
|||||||
@@ -206,6 +206,7 @@ class ExploreViewModel @Inject constructor(
|
|||||||
counter = 0,
|
counter = 0,
|
||||||
progress = null,
|
progress = null,
|
||||||
isFavorite = false,
|
isFavorite = false,
|
||||||
|
isSaved = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ class FavouritesListViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
val result = ArrayList<ListModel>(size + 1)
|
val result = ArrayList<ListModel>(size + 1)
|
||||||
quickFilter.filterItem(filters)?.let(result::add)
|
quickFilter.filterItem(filters)?.let(result::add)
|
||||||
mangaListMapper.toListModelList(result, this, mode)
|
mangaListMapper.toListModelList(result, this, mode, MangaListMapper.NO_FAVORITE)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ class HistoryListViewModel @Inject constructor(
|
|||||||
prevHeader = header
|
prevHeader = header
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result += mangaListMapper.toListModel(manga, mode)
|
result += mangaListMapper.toListModel(manga, mode, 0)
|
||||||
}
|
}
|
||||||
if (filters.isNotEmpty() && isEmpty) {
|
if (filters.isNotEmpty() && isEmpty) {
|
||||||
result += getEmptyState(hasFilters = true)
|
result += getEmptyState(hasFilters = true)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.list.domain
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.annotation.ColorRes
|
import androidx.annotation.ColorRes
|
||||||
|
import androidx.annotation.IntDef
|
||||||
import androidx.collection.MutableScatterSet
|
import androidx.collection.MutableScatterSet
|
||||||
import androidx.collection.ScatterSet
|
import androidx.collection.ScatterSet
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
@@ -15,6 +16,7 @@ import org.koitharu.kotatsu.list.ui.model.MangaCompactListModel
|
|||||||
import org.koitharu.kotatsu.list.ui.model.MangaDetailedListModel
|
import org.koitharu.kotatsu.list.ui.model.MangaDetailedListModel
|
||||||
import org.koitharu.kotatsu.list.ui.model.MangaGridModel
|
import org.koitharu.kotatsu.list.ui.model.MangaGridModel
|
||||||
import org.koitharu.kotatsu.list.ui.model.MangaListModel
|
import org.koitharu.kotatsu.list.ui.model.MangaListModel
|
||||||
|
import org.koitharu.kotatsu.local.data.index.LocalMangaIndex
|
||||||
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.domain.TrackingRepository
|
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||||
@@ -28,59 +30,74 @@ class MangaListMapper @Inject constructor(
|
|||||||
private val trackingRepository: TrackingRepository,
|
private val trackingRepository: TrackingRepository,
|
||||||
private val historyRepository: HistoryRepository,
|
private val historyRepository: HistoryRepository,
|
||||||
private val favouritesRepository: FavouritesRepository,
|
private val favouritesRepository: FavouritesRepository,
|
||||||
|
private val localMangaIndex: LocalMangaIndex,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val dict by lazy { readTagsDict(context) }
|
private val dict by lazy { readTagsDict(context) }
|
||||||
|
|
||||||
suspend fun toListModelList(manga: Collection<Manga>, mode: ListMode): List<MangaListModel> = manga.map {
|
suspend fun toListModelList(
|
||||||
toListModel(it, mode)
|
manga: Collection<Manga>,
|
||||||
|
mode: ListMode,
|
||||||
|
@Flags flags: Int
|
||||||
|
): List<MangaListModel> = manga.map {
|
||||||
|
toListModel(it, mode, flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun toListModelList(
|
suspend fun toListModelList(
|
||||||
destination: MutableCollection<in MangaListModel>,
|
destination: MutableCollection<in MangaListModel>,
|
||||||
manga: Collection<Manga>,
|
manga: Collection<Manga>,
|
||||||
mode: ListMode
|
mode: ListMode,
|
||||||
) = manga.mapTo(destination) {
|
@Flags flags: Int,
|
||||||
toListModel(it, mode)
|
) {
|
||||||
|
manga.mapTo(destination) {
|
||||||
|
toListModel(it, mode, flags)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun toListModel(manga: Manga, mode: ListMode): MangaListModel = when (mode) {
|
suspend fun toListModel(
|
||||||
ListMode.LIST -> toCompactListModel(manga)
|
manga: Manga,
|
||||||
ListMode.DETAILED_LIST -> toDetailedListModel(manga)
|
mode: ListMode,
|
||||||
ListMode.GRID -> toGridModel(manga)
|
@Flags flags: Int
|
||||||
|
): MangaListModel = when (mode) {
|
||||||
|
ListMode.LIST -> toCompactListModel(manga, flags)
|
||||||
|
ListMode.DETAILED_LIST -> toDetailedListModel(manga, flags)
|
||||||
|
ListMode.GRID -> toGridModel(manga, flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun toCompactListModel(manga: Manga) = MangaCompactListModel(
|
suspend fun toCompactListModel(manga: Manga, @Flags flags: 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),
|
counter = getCounter(manga.id, flags),
|
||||||
progress = getProgress(manga.id),
|
progress = getProgress(manga.id, flags),
|
||||||
isFavorite = isFavorite(manga.id),
|
isFavorite = isFavorite(manga.id, flags),
|
||||||
|
isSaved = isSaved(manga.id, flags),
|
||||||
)
|
)
|
||||||
|
|
||||||
suspend fun toDetailedListModel(manga: Manga) = MangaDetailedListModel(
|
suspend fun toDetailedListModel(manga: Manga, @Flags flags: 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),
|
counter = getCounter(manga.id, flags),
|
||||||
progress = getProgress(manga.id),
|
progress = getProgress(manga.id, flags),
|
||||||
isFavorite = isFavorite(manga.id),
|
isFavorite = isFavorite(manga.id, flags),
|
||||||
|
isSaved = isSaved(manga.id, flags),
|
||||||
tags = mapTags(manga.tags),
|
tags = mapTags(manga.tags),
|
||||||
)
|
)
|
||||||
|
|
||||||
suspend fun toGridModel(manga: Manga) = MangaGridModel(
|
suspend fun toGridModel(manga: Manga, @Flags flags: 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),
|
counter = getCounter(manga.id, flags),
|
||||||
progress = getProgress(manga.id),
|
progress = getProgress(manga.id, flags),
|
||||||
isFavorite = isFavorite(manga.id),
|
isFavorite = isFavorite(manga.id, flags),
|
||||||
|
isSaved = isSaved(manga.id, flags),
|
||||||
)
|
)
|
||||||
|
|
||||||
fun mapTags(tags: Collection<MangaTag>) = tags.map {
|
fun mapTags(tags: Collection<MangaTag>) = tags.map {
|
||||||
@@ -91,7 +108,7 @@ class MangaListMapper @Inject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getCounter(mangaId: Long): Int {
|
private suspend fun getCounter(mangaId: Long, @Flags flags: Int): Int {
|
||||||
return if (settings.isTrackerEnabled) {
|
return if (settings.isTrackerEnabled) {
|
||||||
trackingRepository.getNewChaptersCount(mangaId)
|
trackingRepository.getNewChaptersCount(mangaId)
|
||||||
} else {
|
} else {
|
||||||
@@ -99,12 +116,20 @@ class MangaListMapper @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getProgress(mangaId: Long): ReadingProgress? {
|
private suspend fun getProgress(mangaId: Long, @Flags flags: Int): ReadingProgress? {
|
||||||
return historyRepository.getProgress(mangaId, settings.progressIndicatorMode)
|
return if (flags.hasNoFlag(NO_PROGRESS)) {
|
||||||
|
historyRepository.getProgress(mangaId, settings.progressIndicatorMode)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isFavorite(mangaId: Long): Boolean {
|
private suspend fun isFavorite(mangaId: Long, @Flags flags: Int): Boolean {
|
||||||
return false // TODO favouritesRepository.isFavorite(mangaId)
|
return flags.hasNoFlag(NO_FAVORITE) && favouritesRepository.isFavorite(mangaId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun isSaved(mangaId: Long, @Flags flags: Int): Boolean {
|
||||||
|
return flags.hasNoFlag(NO_SAVED) && mangaId in localMangaIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
@ColorRes
|
@ColorRes
|
||||||
@@ -128,4 +153,18 @@ class MangaListMapper @Inject constructor(
|
|||||||
set.trim()
|
set.trim()
|
||||||
set
|
set
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Int.hasNoFlag(flag: Int) = this and flag == 0
|
||||||
|
|
||||||
|
|
||||||
|
@IntDef(0, NO_SAVED, NO_PROGRESS, NO_FAVORITE)
|
||||||
|
@Retention(AnnotationRetention.SOURCE)
|
||||||
|
annotation class Flags
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val NO_SAVED = 1
|
||||||
|
const val NO_PROGRESS = 2
|
||||||
|
const val NO_FAVORITE = 4
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,7 +37,12 @@ fun mangaGridItemAD(
|
|||||||
bind { payloads ->
|
bind { payloads ->
|
||||||
binding.textViewTitle.text = item.title
|
binding.textViewTitle.text = item.title
|
||||||
binding.progressView.setProgress(item.progress, PAYLOAD_PROGRESS_CHANGED in payloads)
|
binding.progressView.setProgress(item.progress, PAYLOAD_PROGRESS_CHANGED in payloads)
|
||||||
binding.imageViewFavorite.isVisible = item.isFavorite
|
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)
|
||||||
|
|||||||
@@ -12,4 +12,5 @@ data class MangaCompactListModel(
|
|||||||
override val counter: Int,
|
override val counter: Int,
|
||||||
override val progress: ReadingProgress?,
|
override val progress: ReadingProgress?,
|
||||||
override val isFavorite: Boolean,
|
override val isFavorite: Boolean,
|
||||||
|
override val isSaved: Boolean,
|
||||||
) : MangaListModel()
|
) : MangaListModel()
|
||||||
|
|||||||
@@ -13,5 +13,6 @@ data class MangaDetailedListModel(
|
|||||||
override val counter: Int,
|
override val counter: Int,
|
||||||
override val progress: ReadingProgress?,
|
override val progress: ReadingProgress?,
|
||||||
override val isFavorite: Boolean,
|
override val isFavorite: Boolean,
|
||||||
|
override val isSaved: Boolean,
|
||||||
val tags: List<ChipsView.ChipModel>,
|
val tags: List<ChipsView.ChipModel>,
|
||||||
) : MangaListModel()
|
) : MangaListModel()
|
||||||
|
|||||||
@@ -11,4 +11,5 @@ data class MangaGridModel(
|
|||||||
override val counter: Int,
|
override val counter: Int,
|
||||||
override val progress: ReadingProgress?,
|
override val progress: ReadingProgress?,
|
||||||
override val isFavorite: Boolean,
|
override val isFavorite: Boolean,
|
||||||
|
override val isSaved: Boolean,
|
||||||
) : MangaListModel()
|
) : MangaListModel()
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ sealed class MangaListModel : ListModel {
|
|||||||
abstract val coverUrl: String?
|
abstract val coverUrl: String?
|
||||||
abstract val counter: Int
|
abstract val counter: Int
|
||||||
abstract val isFavorite: Boolean
|
abstract val isFavorite: Boolean
|
||||||
|
abstract val isSaved: Boolean
|
||||||
abstract val progress: ReadingProgress?
|
abstract val progress: ReadingProgress?
|
||||||
|
|
||||||
val source: MangaSource
|
val source: MangaSource
|
||||||
@@ -27,7 +28,9 @@ sealed class MangaListModel : ListModel {
|
|||||||
previousState !is MangaListModel || previousState.manga != manga -> null
|
previousState !is MangaListModel || previousState.manga != manga -> null
|
||||||
|
|
||||||
previousState.progress != progress -> PAYLOAD_PROGRESS_CHANGED
|
previousState.progress != progress -> PAYLOAD_PROGRESS_CHANGED
|
||||||
previousState.isFavorite != isFavorite || previousState.counter != counter -> PAYLOAD_ANYTHING_CHANGED
|
previousState.isFavorite != isFavorite ||
|
||||||
|
previousState.isSaved != isSaved ||
|
||||||
|
previousState.counter != counter -> PAYLOAD_ANYTHING_CHANGED
|
||||||
|
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,6 +73,10 @@ class LocalMangaIndex @Inject constructor(
|
|||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend operator fun contains(mangaId: Long): Boolean {
|
||||||
|
return db.getLocalMangaIndexDao().findPath(mangaId) != null
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun put(manga: LocalManga) = mutex.withLock {
|
suspend fun put(manga: LocalManga) = mutex.withLock {
|
||||||
db.withTransaction {
|
db.withTransaction {
|
||||||
upsert(manga)
|
upsert(manga)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.SharedFlow
|
|||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
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.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.core.util.ext.toFileOrNull
|
import org.koitharu.kotatsu.core.util.ext.toFileOrNull
|
||||||
@@ -25,6 +26,7 @@ import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
|||||||
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
||||||
import org.koitharu.kotatsu.local.domain.DeleteLocalMangaUseCase
|
import org.koitharu.kotatsu.local.domain.DeleteLocalMangaUseCase
|
||||||
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.remotelist.ui.RemoteListViewModel
|
import org.koitharu.kotatsu.remotelist.ui.RemoteListViewModel
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -107,6 +109,12 @@ class LocalListViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun mapMangaList(
|
||||||
|
destination: MutableCollection<in ListModel>,
|
||||||
|
manga: Collection<Manga>,
|
||||||
|
mode: ListMode
|
||||||
|
) = mangaListMapper.toListModelList(destination, manga, mode, MangaListMapper.NO_SAVED)
|
||||||
|
|
||||||
override fun createEmptyState(canResetFilter: Boolean): EmptyState = if (canResetFilter) {
|
override fun createEmptyState(canResetFilter: Boolean): EmptyState = if (canResetFilter) {
|
||||||
super.createEmptyState(true)
|
super.createEmptyState(true)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import org.koitharu.kotatsu.core.model.MangaSource
|
|||||||
import org.koitharu.kotatsu.core.model.distinctById
|
import org.koitharu.kotatsu.core.model.distinctById
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
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.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.core.util.ext.getCauseUrl
|
import org.koitharu.kotatsu.core.util.ext.getCauseUrl
|
||||||
@@ -36,6 +37,7 @@ 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
|
||||||
@@ -50,7 +52,7 @@ open class RemoteListViewModel @Inject constructor(
|
|||||||
mangaRepositoryFactory: MangaRepository.Factory,
|
mangaRepositoryFactory: MangaRepository.Factory,
|
||||||
final override val filterCoordinator: FilterCoordinator,
|
final override val filterCoordinator: FilterCoordinator,
|
||||||
settings: AppSettings,
|
settings: AppSettings,
|
||||||
mangaListMapper: MangaListMapper,
|
protected val mangaListMapper: MangaListMapper,
|
||||||
downloadScheduler: DownloadWorker.Scheduler,
|
downloadScheduler: DownloadWorker.Scheduler,
|
||||||
private val exploreRepository: ExploreRepository,
|
private val exploreRepository: ExploreRepository,
|
||||||
sourcesRepository: MangaSourcesRepository,
|
sourcesRepository: MangaSourcesRepository,
|
||||||
@@ -85,7 +87,7 @@ open class RemoteListViewModel @Inject constructor(
|
|||||||
list == null -> add(LoadingState)
|
list == null -> add(LoadingState)
|
||||||
list.isEmpty() -> add(createEmptyState(canResetFilter = filterCoordinator.isFilterApplied))
|
list.isEmpty() -> add(createEmptyState(canResetFilter = filterCoordinator.isFilterApplied))
|
||||||
else -> {
|
else -> {
|
||||||
mangaListMapper.toListModelList(this, list, mode)
|
mapMangaList(this, list, mode)
|
||||||
when {
|
when {
|
||||||
error != null -> add(error.toErrorFooter())
|
error != null -> add(error.toErrorFooter())
|
||||||
hasNext -> add(LoadingFooter())
|
hasNext -> add(LoadingFooter())
|
||||||
@@ -171,6 +173,12 @@ open class RemoteListViewModel @Inject constructor(
|
|||||||
|
|
||||||
protected open suspend fun onBuildList(list: MutableList<ListModel>) = Unit
|
protected open suspend fun onBuildList(list: MutableList<ListModel>) = Unit
|
||||||
|
|
||||||
|
protected open suspend fun mapMangaList(
|
||||||
|
destination: MutableCollection<in ListModel>,
|
||||||
|
manga: Collection<Manga>,
|
||||||
|
mode: ListMode
|
||||||
|
) = mangaListMapper.toListModelList(destination, manga, mode, 0)
|
||||||
|
|
||||||
fun openRandom() {
|
fun openRandom() {
|
||||||
if (randomJob?.isActive == true) {
|
if (randomJob?.isActive == true) {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ 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(
|
||||||
@@ -161,7 +162,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),
|
list = mangaListMapper.toListModelList(manga = result, mode = ListMode.GRID, flags = 0),
|
||||||
error = null,
|
error = null,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -190,7 +191,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),
|
list = mangaListMapper.toListModelList(manga = result, mode = ListMode.GRID, flags = 0),
|
||||||
error = null,
|
error = null,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -219,7 +220,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),
|
list = mangaListMapper.toListModelList(manga = result, mode = ListMode.GRID,flags = 0),
|
||||||
error = null,
|
error = null,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.content.Intent
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
|
||||||
|
//FIXME: https://stackoverflow.com/questions/77555641/saf-no-activity-found-to-handle-intent-android-intent-action-open-document-tr
|
||||||
class PickDirectoryContract : ActivityResultContracts.OpenDocumentTree() {
|
class PickDirectoryContract : ActivityResultContracts.OpenDocumentTree() {
|
||||||
|
|
||||||
override fun createIntent(context: Context, input: Uri?): Intent {
|
override fun createIntent(context: Context, input: Uri?): Intent {
|
||||||
|
|||||||
@@ -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)
|
mangaListMapper.toListModelList(this, list, mode, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.onStart {
|
}.onStart {
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ class FeedViewModel @Inject constructor(
|
|||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
UpdatedMangaHeader(
|
UpdatedMangaHeader(
|
||||||
mangaList.map { mangaListMapper.toGridModel(it.manga) },
|
mangaList.map { mangaListMapper.toGridModel(it.manga, 0) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ class UpdatesViewModel @Inject constructor(
|
|||||||
prevHeader = header
|
prevHeader = header
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result += mangaListMapper.toListModel(item.manga, mode)
|
result += mangaListMapper.toListModel(item.manga, mode, 0)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
10
app/src/main/res/drawable/bg_list_icons.xml
Normal file
10
app/src/main/res/drawable/bg_list_icons.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
|
||||||
|
<corners
|
||||||
|
android:bottomRightRadius="4dp"
|
||||||
|
android:topRightRadius="4dp" />
|
||||||
|
<solid android:color="?colorBackgroundFloating" />
|
||||||
|
</shape>
|
||||||
@@ -34,16 +34,17 @@
|
|||||||
android:layout_gravity="bottom|end"
|
android:layout_gravity="bottom|end"
|
||||||
android:layout_margin="@dimen/card_indicator_offset" />
|
android:layout_margin="@dimen/card_indicator_offset" />
|
||||||
|
|
||||||
<ImageView
|
<org.koitharu.kotatsu.core.ui.widgets.IconsView
|
||||||
android:id="@+id/imageView_favorite"
|
android:id="@+id/iconsView"
|
||||||
android:layout_width="@dimen/card_indicator_size"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="@dimen/card_indicator_size"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="bottom|start"
|
android:layout_gravity="bottom|start"
|
||||||
android:layout_margin="@dimen/card_indicator_offset"
|
android:layout_marginBottom="@dimen/card_indicator_offset"
|
||||||
android:contentDescription="@string/favourites"
|
android:background="@drawable/bg_list_icons"
|
||||||
android:scaleType="centerInside"
|
android:orientation="horizontal"
|
||||||
app:srcCompat="@drawable/ic_heart"
|
android:padding="4dp"
|
||||||
app:tint="?colorSurfaceBright" />
|
app:iconSize="12dp"
|
||||||
|
app:iconSpacing="2dp" />
|
||||||
|
|
||||||
<org.koitharu.kotatsu.core.ui.widgets.BadgeView
|
<org.koitharu.kotatsu.core.ui.widgets.BadgeView
|
||||||
android:id="@+id/badge"
|
android:id="@+id/badge"
|
||||||
|
|||||||
@@ -178,6 +178,11 @@
|
|||||||
<attr name="android:progress" />
|
<attr name="android:progress" />
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
|
|
||||||
|
<declare-styleable name="IconsView">
|
||||||
|
<attr name="iconSize" />
|
||||||
|
<attr name="iconSpacing" format="dimension" />
|
||||||
|
</declare-styleable>
|
||||||
|
|
||||||
<declare-styleable name="FilterFieldLayout">
|
<declare-styleable name="FilterFieldLayout">
|
||||||
<attr name="title" />
|
<attr name="title" />
|
||||||
<attr name="showMoreButton" format="boolean" />
|
<attr name="showMoreButton" format="boolean" />
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ material = "1.13.0-alpha10"
|
|||||||
moshi = "1.15.2"
|
moshi = "1.15.2"
|
||||||
okhttp = "4.12.0"
|
okhttp = "4.12.0"
|
||||||
okio = "3.10.2"
|
okio = "3.10.2"
|
||||||
parsers = "794a737b6d"
|
parsers = "198e859850"
|
||||||
preference = "1.2.1"
|
preference = "1.2.1"
|
||||||
recyclerview = "1.4.0"
|
recyclerview = "1.4.0"
|
||||||
room = "2.6.1"
|
room = "2.6.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user