Improve quick filters

This commit is contained in:
Koitharu
2024-09-23 14:36:29 +03:00
parent 169e31e9ba
commit b1eabdba79
31 changed files with 275 additions and 103 deletions

View File

@@ -43,6 +43,8 @@ fun MangaSource(name: String?): MangaSource {
return UnknownMangaSource return UnknownMangaSource
} }
fun Collection<String>.toMangaSources() = map(::MangaSource)
fun MangaSource.isNsfw(): Boolean = when (this) { fun MangaSource.isNsfw(): Boolean = when (this) {
is MangaSourceInfo -> mangaSource.isNsfw() is MangaSourceInfo -> mangaSource.isNsfw()
is MangaParserSource -> contentType == ContentType.HENTAI is MangaParserSource -> contentType == ContentType.HENTAI
@@ -61,11 +63,16 @@ val ContentType.titleResId
ContentType.NOVEL -> R.string.content_type_novel ContentType.NOVEL -> R.string.content_type_novel
} }
fun MangaSource.getSummary(context: Context): String? = when (this) { tailrec fun MangaSource.unwrap(): MangaSource = if (this is MangaSourceInfo) {
is MangaSourceInfo -> mangaSource.getSummary(context) mangaSource.unwrap()
} else {
this
}
fun MangaSource.getSummary(context: Context): String? = when (val source = unwrap()) {
is MangaParserSource -> { is MangaParserSource -> {
val type = context.getString(contentType.titleResId) val type = context.getString(source.contentType.titleResId)
val locale = locale.toLocale().getDisplayName(context) val locale = source.locale.toLocale().getDisplayName(context)
context.getString(R.string.source_summary_pattern, type, locale) context.getString(R.string.source_summary_pattern, type, locale)
} }
@@ -74,11 +81,10 @@ fun MangaSource.getSummary(context: Context): String? = when (this) {
else -> null else -> null
} }
fun MangaSource.getTitle(context: Context): String = when (this) { fun MangaSource.getTitle(context: Context): String = when (val source = unwrap()) {
is MangaSourceInfo -> mangaSource.getTitle(context) is MangaParserSource -> source.title
is MangaParserSource -> title
LocalMangaSource -> context.getString(R.string.local_storage) LocalMangaSource -> context.getString(R.string.local_storage)
is ExternalMangaSource -> resolveName(context) is ExternalMangaSource -> source.resolveName(context)
else -> context.getString(R.string.unknown) else -> context.getString(R.string.unknown)
} }

View File

@@ -114,6 +114,7 @@ class FaviconFetcher(
.url(url) .url(url)
.get() .get()
.tag(MangaSource::class.java, source) .tag(MangaSource::class.java, source)
request.tag(MangaSource::class.java, source)
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
options.tags.asMap().forEach { request.tag(it.key as Class<Any>, it.value) } options.tags.asMap().forEach { request.tag(it.key as Class<Any>, it.value) }
val response = okHttpClient.newCall(request.build()).await() val response = okHttpClient.newCall(request.build()).await()

View File

@@ -8,18 +8,32 @@ import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.core.view.children import androidx.core.view.children
import coil.ImageLoader
import coil.request.Disposable
import coil.request.ImageRequest
import coil.transform.RoundedCornersTransformation
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipDrawable import com.google.android.material.chip.ChipDrawable
import com.google.android.material.chip.ChipGroup import com.google.android.material.chip.ChipGroup
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.image.ChipIconTarget
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.setProgressIcon
import org.koitharu.kotatsu.parsers.util.ifZero
import javax.inject.Inject
import com.google.android.material.R as materialR import com.google.android.material.R as materialR
@AndroidEntryPoint
class ChipsView @JvmOverloads constructor( class ChipsView @JvmOverloads constructor(
context: Context, context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = com.google.android.material.R.attr.chipGroupStyle, defStyleAttr: Int = com.google.android.material.R.attr.chipGroupStyle,
) : ChipGroup(context, attrs, defStyleAttr) { ) : ChipGroup(context, attrs, defStyleAttr) {
@Inject
lateinit var coil: ImageLoader
private var isLayoutSuppressedCompat = false private var isLayoutSuppressedCompat = false
private var isLayoutCalledOnSuppressed = false private var isLayoutCalledOnSuppressed = false
private val chipOnClickListener = InternalChipClickListener() private val chipOnClickListener = InternalChipClickListener()
@@ -90,8 +104,10 @@ class ChipsView @JvmOverloads constructor(
val title: CharSequence? = null, val title: CharSequence? = null,
@StringRes val titleResId: Int = 0, @StringRes val titleResId: Int = 0,
@DrawableRes val icon: Int = 0, @DrawableRes val icon: Int = 0,
val iconData: Any? = null,
@ColorRes val tint: Int = 0, @ColorRes val tint: Int = 0,
val isChecked: Boolean = false, val isChecked: Boolean = false,
val isLoading: Boolean = false,
val isDropdown: Boolean = false, val isDropdown: Boolean = false,
val isCloseable: Boolean = false, val isCloseable: Boolean = false,
val data: Any? = null, val data: Any? = null,
@@ -100,6 +116,7 @@ class ChipsView @JvmOverloads constructor(
private inner class DataChip(context: Context) : Chip(context) { private inner class DataChip(context: Context) : Chip(context) {
private var model: ChipModel? = null private var model: ChipModel? = null
private var imageRequest: Disposable? = null
init { init {
val drawable = ChipDrawable.createFromAttributes(context, null, 0, chipStyle) val drawable = ChipDrawable.createFromAttributes(context, null, 0, chipStyle)
@@ -112,6 +129,9 @@ class ChipsView @JvmOverloads constructor(
} }
fun bind(model: ChipModel) { fun bind(model: ChipModel) {
if (this.model == model) {
return
}
this.model = model this.model = model
if (model.titleResId == 0) { if (model.titleResId == 0) {
@@ -127,13 +147,7 @@ class ChipsView @JvmOverloads constructor(
isChecked = false isChecked = false
isCheckable = false isCheckable = false
} }
if (model.icon == 0 || model.isChecked) { bindIcon(model)
chipIcon = null
isChipIconVisible = false
} else {
setChipIconResource(model.icon)
isChipIconVisible = true
}
isCheckedIconVisible = model.isChecked isCheckedIconVisible = model.isChecked
isCloseIconVisible = if (model.isCloseable || model.isDropdown) { isCloseIconVisible = if (model.isCloseable || model.isDropdown) {
setCloseIconResource( setCloseIconResource(
@@ -147,6 +161,54 @@ class ChipsView @JvmOverloads constructor(
} }
override fun toggle() = Unit override fun toggle() = Unit
private fun bindIcon(model: ChipModel) {
when {
model.isChecked -> {
imageRequest?.dispose()
imageRequest = null
chipIcon = null
isChipIconVisible = false
}
model.isLoading -> {
imageRequest?.dispose()
imageRequest = null
isChipIconVisible = true
setProgressIcon()
}
model.iconData != null -> {
val placeholder = model.icon.ifZero { materialR.drawable.navigation_empty_icon }
imageRequest = ImageRequest.Builder(context)
.data(model.iconData)
.crossfade(false)
.size(resources.getDimensionPixelSize(materialR.dimen.m3_chip_icon_size))
.target(ChipIconTarget(this))
.placeholder(placeholder)
.fallback(placeholder)
.error(placeholder)
.transformations(RoundedCornersTransformation(resources.getDimension(R.dimen.chip_icon_corner)))
.allowRgb565(true)
.enqueueWith(coil)
isChipIconVisible = true
}
model.icon != 0 -> {
imageRequest?.dispose()
imageRequest = null
setChipIconResource(model.icon)
isChipIconVisible = true
}
else -> {
imageRequest?.dispose()
imageRequest = null
chipIcon = null
isChipIconVisible = false
}
}
}
} }
private inner class InternalChipClickListener : OnClickListener { private inner class InternalChipClickListener : OnClickListener {

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.core.util.ext package org.koitharu.kotatsu.core.util.ext
import android.content.Context import android.content.Context
import android.database.DatabaseUtils
import androidx.annotation.FloatRange import androidx.annotation.FloatRange
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.parsers.util.ellipsize import org.koitharu.kotatsu.parsers.util.ellipsize
@@ -64,3 +65,11 @@ fun <T> Collection<T>.joinToStringWithLimit(context: Context, limit: Int, transf
} }
} }
} }
@Deprecated("",
ReplaceWith(
"sqlEscapeString(this)",
"android.database.DatabaseUtils.sqlEscapeString"
)
)
fun String.sqlEscape(): String = DatabaseUtils.sqlEscapeString(this)

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.favourites.data package org.koitharu.kotatsu.favourites.data
import android.database.DatabaseUtils.sqlEscapeString
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
@@ -120,6 +121,12 @@ abstract class FavouritesDao : MangaQueryBuilder.ConditionCallback {
@Query("SELECT COUNT(category_id) FROM favourites WHERE manga_id = :mangaId AND deleted_at = 0") @Query("SELECT COUNT(category_id) FROM favourites WHERE manga_id = :mangaId AND deleted_at = 0")
abstract suspend fun findCategoriesCount(mangaId: Long): Int abstract suspend fun findCategoriesCount(mangaId: Long): Int
@Query("SELECT manga.source AS count FROM favourites LEFT JOIN manga ON manga.manga_id = favourites.manga_id GROUP BY manga.source ORDER BY COUNT(manga.source) DESC LIMIT :limit")
abstract suspend fun findPopularSources(limit: Int): List<String>
@Query("SELECT manga.source AS count FROM favourites LEFT JOIN manga ON manga.manga_id = favourites.manga_id WHERE favourites.category_id = :categoryId GROUP BY manga.source ORDER BY COUNT(manga.source) DESC LIMIT :limit")
abstract suspend fun findPopularSources(categoryId: Long, limit: Int): List<String>
/** INSERT **/ /** INSERT **/
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
@@ -200,6 +207,7 @@ abstract class FavouritesDao : MangaQueryBuilder.ConditionCallback {
ListFilterOption.Macro.NSFW -> "manga.nsfw = 1" ListFilterOption.Macro.NSFW -> "manga.nsfw = 1"
is ListFilterOption.Tag -> "EXISTS(SELECT * FROM manga_tags WHERE favourites.manga_id = manga_tags.manga_id AND tag_id = ${option.tagId})" is ListFilterOption.Tag -> "EXISTS(SELECT * FROM manga_tags WHERE favourites.manga_id = manga_tags.manga_id AND tag_id = ${option.tagId})"
ListFilterOption.Downloaded -> "EXISTS(SELECT * FROM local_index WHERE local_index.manga_id = favourites.manga_id)" ListFilterOption.Downloaded -> "EXISTS(SELECT * FROM local_index WHERE local_index.manga_id = favourites.manga_id)"
is ListFilterOption.Source -> "manga.source = ${sqlEscapeString(option.mangaSource.name)}"
else -> null else -> null
} }
} }

View File

@@ -1,12 +1,15 @@
package org.koitharu.kotatsu.favourites.domain package org.koitharu.kotatsu.favourites.domain
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import org.koitharu.kotatsu.core.os.NetworkState import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.list.domain.ListFilterOption import org.koitharu.kotatsu.list.domain.ListFilterOption
import org.koitharu.kotatsu.list.domain.MangaListQuickFilter import org.koitharu.kotatsu.list.domain.MangaListQuickFilter
import javax.inject.Inject
class FavoritesListQuickFilter @Inject constructor( class FavoritesListQuickFilter @AssistedInject constructor(
@Assisted private val categoryId: Long,
private val settings: AppSettings, private val settings: AppSettings,
private val repository: FavouritesRepository, private val repository: FavouritesRepository,
networkState: NetworkState, networkState: NetworkState,
@@ -22,5 +25,14 @@ class FavoritesListQuickFilter @Inject constructor(
add(ListFilterOption.Macro.NEW_CHAPTERS) add(ListFilterOption.Macro.NEW_CHAPTERS)
} }
add(ListFilterOption.Macro.COMPLETED) add(ListFilterOption.Macro.COMPLETED)
repository.findPopularSources(categoryId, 3).mapTo(this) {
ListFilterOption.Source(it)
}
}
@AssistedFactory
interface Factory {
fun create(categoryId: Long): FavoritesListQuickFilter
} }
} }

View File

@@ -11,6 +11,8 @@ import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.toEntities import org.koitharu.kotatsu.core.db.entity.toEntities
import org.koitharu.kotatsu.core.db.entity.toEntity import org.koitharu.kotatsu.core.db.entity.toEntity
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.toMangaSources
import org.koitharu.kotatsu.core.ui.util.ReversibleHandle import org.koitharu.kotatsu.core.ui.util.ReversibleHandle
import org.koitharu.kotatsu.core.util.ext.mapItems import org.koitharu.kotatsu.core.util.ext.mapItems
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
@@ -22,6 +24,7 @@ import org.koitharu.kotatsu.favourites.domain.model.Cover
import org.koitharu.kotatsu.list.domain.ListFilterOption import org.koitharu.kotatsu.list.domain.ListFilterOption
import org.koitharu.kotatsu.list.domain.ListSortOrder import org.koitharu.kotatsu.list.domain.ListSortOrder
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import javax.inject.Inject import javax.inject.Inject
@Reusable @Reusable
@@ -136,6 +139,16 @@ class FavouritesRepository @Inject constructor(
return db.getFavouritesDao().findCategoriesIds(mangaId).toSet() return db.getFavouritesDao().findCategoriesIds(mangaId).toSet()
} }
suspend fun findPopularSources(categoryId: Long, limit: Int): List<MangaSource> {
return db.getFavouritesDao().run {
if (categoryId == 0L) {
findPopularSources(limit)
} else {
findPopularSources(categoryId, limit)
}
}.toMangaSources()
}
suspend fun createCategory( suspend fun createCategory(
title: String, title: String,
sortOrder: ListSortOrder, sortOrder: ListSortOrder,

View File

@@ -6,11 +6,13 @@ import android.graphics.Color
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.view.View import android.view.View
import androidx.core.graphics.ColorUtils import androidx.core.graphics.ColorUtils
import androidx.core.view.isVisible
import androidx.core.widget.ImageViewCompat import androidx.core.widget.ImageViewCompat
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
import org.koitharu.kotatsu.core.util.ext.getThemeColor import org.koitharu.kotatsu.core.util.ext.getThemeColor
@@ -64,14 +66,20 @@ fun categoriesHeaderAD(
repeat(coverViews.size) { i -> repeat(coverViews.size) { i ->
val cover = item.covers.getOrNull(i) val cover = item.covers.getOrNull(i)
coverViews[i].newImageRequest(lifecycleOwner, cover?.url)?.run { val view = coverViews[i]
placeholder(R.drawable.ic_placeholder) view.isVisible = cover != null
fallback(fallback) if (cover == null) {
source(cover?.mangaSource) view.disposeImageRequest()
crossfade(crossFadeDuration * (i + 1)) } else {
error(R.drawable.ic_error_placeholder) view.newImageRequest(lifecycleOwner, cover.url)?.run {
allowRgb565(true) placeholder(R.drawable.ic_placeholder)
enqueueWith(coil) fallback(fallback)
source(cover.mangaSource)
crossfade(crossFadeDuration * (i + 1))
error(R.drawable.ic_error_placeholder)
allowRgb565(true)
enqueueWith(coil)
}
} }
} }
} }

View File

@@ -49,12 +49,13 @@ class FavouritesListViewModel @Inject constructor(
private val repository: FavouritesRepository, private val repository: FavouritesRepository,
private val mangaListMapper: MangaListMapper, private val mangaListMapper: MangaListMapper,
private val markAsReadUseCase: MarkAsReadUseCase, private val markAsReadUseCase: MarkAsReadUseCase,
private val quickFilter: FavoritesListQuickFilter, quickFilterFactory: FavoritesListQuickFilter.Factory,
settings: AppSettings, settings: AppSettings,
downloadScheduler: DownloadWorker.Scheduler, downloadScheduler: DownloadWorker.Scheduler,
) : MangaListViewModel(settings, downloadScheduler), QuickFilterListener by quickFilter { ) : MangaListViewModel(settings, downloadScheduler), QuickFilterListener {
val categoryId: Long = savedStateHandle[ARG_CATEGORY_ID] ?: NO_ID val categoryId: Long = savedStateHandle[ARG_CATEGORY_ID] ?: NO_ID
private val quickFilter = quickFilterFactory.create(categoryId)
private val refreshTrigger = MutableStateFlow(Any()) private val refreshTrigger = MutableStateFlow(Any())
private val limit = MutableStateFlow(PAGE_SIZE) private val limit = MutableStateFlow(PAGE_SIZE)
private val isPaginationReady = AtomicBoolean(false) private val isPaginationReady = AtomicBoolean(false)
@@ -91,6 +92,12 @@ class FavouritesListViewModel @Inject constructor(
override fun onRetry() = Unit override fun onRetry() = Unit
override fun setFilterOption(option: ListFilterOption, isApplied: Boolean) = quickFilter.setFilterOption(option, isApplied)
override fun toggleFilterOption(option: ListFilterOption) = quickFilter.toggleFilterOption(option)
override fun clearFilter() = quickFilter.clearFilter()
fun markAsRead(items: Set<Manga>) { fun markAsRead(items: Set<Manga>) {
launchLoadingJob(Dispatchers.Default) { launchLoadingJob(Dispatchers.Default) {
markAsReadUseCase(items) markAsReadUseCase(items)

View File

@@ -66,7 +66,7 @@ class FilterCoordinator @Inject constructor(
get() = repository.source get() = repository.source
val isFilterApplied: Boolean val isFilterApplied: Boolean
get() = !currentListFilter.value.isEmpty() get() = currentListFilter.value.isNotEmpty()
val query: StateFlow<String?> = currentListFilter.map { it.query } val query: StateFlow<String?> = currentListFilter.map { it.query }
.stateIn(coroutineScope, SharingStarted.Eagerly, null) .stateIn(coroutineScope, SharingStarted.Eagerly, null)

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.history.data package org.koitharu.kotatsu.history.data
import android.database.DatabaseUtils.sqlEscapeString
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
@@ -73,6 +74,9 @@ abstract class HistoryDao : MangaQueryBuilder.ConditionCallback {
) )
abstract suspend fun findPopularTags(limit: Int): List<TagEntity> abstract suspend fun findPopularTags(limit: Int): List<TagEntity>
@Query("SELECT manga.source AS count FROM history LEFT JOIN manga ON manga.manga_id = history.manga_id GROUP BY manga.source ORDER BY COUNT(manga.source) DESC LIMIT :limit")
abstract suspend fun findPopularSources(limit: Int): List<String>
@Query("SELECT * FROM history WHERE manga_id = :id AND deleted_at = 0") @Query("SELECT * FROM history WHERE manga_id = :id AND deleted_at = 0")
abstract suspend fun find(id: Long): HistoryEntity? abstract suspend fun find(id: Long): HistoryEntity?
@@ -160,6 +164,7 @@ abstract class HistoryDao : MangaQueryBuilder.ConditionCallback {
ListFilterOption.Macro.NSFW -> "manga.nsfw = 1" ListFilterOption.Macro.NSFW -> "manga.nsfw = 1"
is ListFilterOption.Tag -> "EXISTS(SELECT * FROM manga_tags WHERE history.manga_id = manga_tags.manga_id AND tag_id = ${option.tagId})" is ListFilterOption.Tag -> "EXISTS(SELECT * FROM manga_tags WHERE history.manga_id = manga_tags.manga_id AND tag_id = ${option.tagId})"
ListFilterOption.Downloaded -> "EXISTS(SELECT * FROM local_index WHERE local_index.manga_id = history.manga_id)" ListFilterOption.Downloaded -> "EXISTS(SELECT * FROM local_index WHERE local_index.manga_id = history.manga_id)"
is ListFilterOption.Source -> "manga.source = ${sqlEscapeString(option.mangaSource.name)}"
else -> null else -> null
} }
} }

View File

@@ -12,10 +12,13 @@ import org.koitharu.kotatsu.core.db.entity.toEntity
import org.koitharu.kotatsu.core.db.entity.toManga import org.koitharu.kotatsu.core.db.entity.toManga
import org.koitharu.kotatsu.core.db.entity.toMangaTag import org.koitharu.kotatsu.core.db.entity.toMangaTag
import org.koitharu.kotatsu.core.db.entity.toMangaTags import org.koitharu.kotatsu.core.db.entity.toMangaTags
import org.koitharu.kotatsu.core.db.entity.toMangaTagsList
import org.koitharu.kotatsu.core.model.MangaHistory import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.findById import org.koitharu.kotatsu.core.model.findById
import org.koitharu.kotatsu.core.model.isLocal import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.model.isNsfw import org.koitharu.kotatsu.core.model.isNsfw
import org.koitharu.kotatsu.core.model.toMangaSources
import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ProgressIndicatorMode import org.koitharu.kotatsu.core.prefs.ProgressIndicatorMode
@@ -26,6 +29,7 @@ import org.koitharu.kotatsu.list.domain.ListFilterOption
import org.koitharu.kotatsu.list.domain.ListSortOrder import org.koitharu.kotatsu.list.domain.ListSortOrder
import org.koitharu.kotatsu.list.domain.ReadingProgress import org.koitharu.kotatsu.list.domain.ReadingProgress
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.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
import org.koitharu.kotatsu.scrobbling.common.domain.tryScrobble import org.koitharu.kotatsu.scrobbling.common.domain.tryScrobble
@@ -177,7 +181,11 @@ class HistoryRepository @Inject constructor(
} }
suspend fun getPopularTags(limit: Int): List<MangaTag> { suspend fun getPopularTags(limit: Int): List<MangaTag> {
return db.getHistoryDao().findPopularTags(limit).map { x -> x.toMangaTag() } return db.getHistoryDao().findPopularTags(limit).toMangaTagsList()
}
suspend fun getPopularSources(limit: Int): List<MangaSource> {
return db.getHistoryDao().findPopularSources(limit).toMangaSources()
} }
fun shouldSkip(manga: Manga): Boolean { fun shouldSkip(manga: Manga): Boolean {

View File

@@ -31,5 +31,8 @@ class HistoryListQuickFilter @Inject constructor(
repository.getPopularTags(3).mapTo(this) { repository.getPopularTags(3).mapTo(this) {
ListFilterOption.Tag(it) ListFilterOption.Tag(it)
} }
repository.getPopularSources(3).mapTo(this) {
ListFilterOption.Source(it)
}
} }
} }

View File

@@ -5,6 +5,12 @@ import androidx.annotation.StringRes
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.db.entity.toEntity import org.koitharu.kotatsu.core.db.entity.toEntity
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.model.LocalMangaSource
import org.koitharu.kotatsu.core.model.unwrap
import org.koitharu.kotatsu.core.parser.external.ExternalMangaSource
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
sealed interface ListFilterOption { sealed interface ListFilterOption {
@@ -19,6 +25,8 @@ sealed interface ListFilterOption {
val groupKey: String val groupKey: String
fun getIconData(): Any? = null
data object Downloaded : ListFilterOption { data object Downloaded : ListFilterOption {
override val titleResId: Int override val titleResId: Int
@@ -88,6 +96,31 @@ sealed interface ListFilterOption {
get() = "_favcat" get() = "_favcat"
} }
data class Source(
val mangaSource: MangaSource
) : ListFilterOption {
override val titleResId: Int
get() = when (mangaSource.unwrap()) {
is ExternalMangaSource -> R.string.external_source
LocalMangaSource -> R.string.local_storage
else -> 0
}
override val iconResId: Int
get() = R.drawable.ic_web
override val titleText: CharSequence?
get() = when (val source = mangaSource.unwrap()) {
is MangaParserSource -> source.title
else -> null
}
override val groupKey: String
get() = "_source"
override fun getIconData() = mangaSource.faviconUri()
}
data class Inverted( data class Inverted(
val option: ListFilterOption, val option: ListFilterOption,
override val iconResId: Int, override val iconResId: Int,

View File

@@ -55,6 +55,7 @@ abstract class MangaListQuickFilter(
title = option.titleText, title = option.titleText,
titleResId = option.titleResId, titleResId = option.titleResId,
icon = option.iconResId, icon = option.iconResId,
iconData = option.getIconData(),
isChecked = option in selectedOptions, isChecked = option in selectedOptions,
data = option, data = option,
) )

View File

@@ -251,10 +251,6 @@ abstract class MangaListFragment :
resolveException(error) resolveException(error)
} }
override fun onUpdateFilter(tags: Set<MangaTag>) {
viewModel.onUpdateFilter(tags)
}
private fun onGridScaleChanged(scale: Float) { private fun onGridScaleChanged(scale: Float) {
spanSizeLookup.invalidateCache() spanSizeLookup.invalidateCache()
spanResolver?.setGridSize(scale, requireViewBinding().recyclerView) spanResolver?.setGridSize(scale, requireViewBinding().recyclerView)

View File

@@ -43,8 +43,6 @@ abstract class MangaListViewModel(
val isIncognitoModeEnabled: Boolean val isIncognitoModeEnabled: Boolean
get() = settings.isIncognitoModeEnabled get() = settings.isIncognitoModeEnabled
open fun onUpdateFilter(tags: Set<MangaTag>) = Unit
abstract fun onRefresh() abstract fun onRefresh()
abstract fun onRetry() abstract fun onRetry()

View File

@@ -2,12 +2,9 @@ package org.koitharu.kotatsu.list.ui.adapter
import android.view.View import android.view.View
import org.koitharu.kotatsu.core.ui.widgets.TipView import org.koitharu.kotatsu.core.ui.widgets.TipView
import org.koitharu.kotatsu.parsers.model.MangaTag
interface MangaListListener : MangaDetailsClickListener, ListStateHolderListener, ListHeaderClickListener, interface MangaListListener : MangaDetailsClickListener, ListStateHolderListener, ListHeaderClickListener,
TipView.OnButtonClickListener, QuickFilterClickListener { TipView.OnButtonClickListener, QuickFilterClickListener {
fun onUpdateFilter(tags: Set<MangaTag>)
fun onFilterClick(view: View?) fun onFilterClick(view: View?)
} }

View File

@@ -4,7 +4,7 @@ import android.content.Context
import androidx.annotation.StringRes import androidx.annotation.StringRes
import org.koitharu.kotatsu.core.ui.model.DateTimeAgo import org.koitharu.kotatsu.core.ui.model.DateTimeAgo
@Suppress("DataClassPrivateConstructor") @ExposedCopyVisibility
data class ListHeader private constructor( data class ListHeader private constructor(
private val textRaw: Any, private val textRaw: Any,
@StringRes val buttonTextRes: Int, @StringRes val buttonTextRes: Int,

View File

@@ -7,6 +7,8 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
@@ -30,6 +32,7 @@ class LocalMangaIndex @Inject constructor(
) : FlowCollector<LocalManga?> { ) : FlowCollector<LocalManga?> {
private val prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) private val prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
private val mutex = Mutex()
private var previousHash: Long private var previousHash: Long
get() = prefs.getLong(KEY_HASH, 0L) get() = prefs.getLong(KEY_HASH, 0L)
@@ -41,7 +44,7 @@ class LocalMangaIndex @Inject constructor(
} }
} }
suspend fun update(): Boolean { suspend fun update(): Boolean = mutex.withLock {
val newHash = computeHash() val newHash = computeHash()
if (newHash == previousHash) { if (newHash == previousHash) {
return false return false
@@ -57,7 +60,13 @@ class LocalMangaIndex @Inject constructor(
} }
suspend fun get(mangaId: Long): LocalManga? { suspend fun get(mangaId: Long): LocalManga? {
val path = db.getLocalMangaIndexDao().findPath(mangaId) ?: return null var path = db.getLocalMangaIndexDao().findPath(mangaId)
if (path == null && mutex.isLocked) { // wait for updating complete
path = mutex.withLock { db.getLocalMangaIndexDao().findPath(mangaId) }
}
if (path == null) {
return null
}
return runCatchingCancellable { return runCatchingCancellable {
LocalMangaInput.of(File(path)).getManga() LocalMangaInput.of(File(path)).getManga()
}.onFailure { }.onFailure {
@@ -65,9 +74,11 @@ class LocalMangaIndex @Inject constructor(
}.getOrNull() }.getOrNull()
} }
suspend fun put(manga: LocalManga) = db.withTransaction { suspend fun put(manga: LocalManga) = mutex.withLock {
mangaDataRepository.storeManga(manga.manga) db.withTransaction {
db.getLocalMangaIndexDao().upsert(manga.toEntity()) mangaDataRepository.storeManga(manga.manga)
db.getLocalMangaIndexDao().upsert(manga.toEntity())
}
} }
suspend fun delete(mangaId: Long) { suspend fun delete(mangaId: Long) {

View File

@@ -108,7 +108,7 @@ class LocalListViewModel @Inject constructor(
} }
override fun createEmptyState(canResetFilter: Boolean): EmptyState = if (canResetFilter) { override fun createEmptyState(canResetFilter: Boolean): EmptyState = if (canResetFilter) {
super.createEmptyState(canResetFilter) super.createEmptyState(true)
} else { } else {
EmptyState( EmptyState(
icon = R.drawable.ic_empty_local, icon = R.drawable.ic_empty_local,

View File

@@ -66,21 +66,32 @@ class RemoteListFragment : MangaListFragment(), FilterCoordinator.Owner {
} }
override fun onEmptyActionClick() { override fun onEmptyActionClick() {
viewModel.resetFilter() if (filterCoordinator.isFilterApplied) {
filterCoordinator.reset()
} else {
openInBrowser()
}
} }
override fun onSecondaryErrorActionClick(error: Throwable) { override fun onSecondaryErrorActionClick(error: Throwable) {
viewModel.browserUrl?.also { url -> openInBrowser()
}
private fun openInBrowser() {
val browserUrl = viewModel.browserUrl
if (browserUrl.isNullOrEmpty()) {
Snackbar.make(requireViewBinding().recyclerView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT)
.show()
} else {
startActivity( startActivity(
BrowserActivity.newIntent( BrowserActivity.newIntent(
requireContext(), requireContext(),
url, browserUrl,
viewModel.source, viewModel.source,
viewModel.source.getTitle(requireContext()), viewModel.source.getTitle(requireContext()),
), ),
) )
} ?: Snackbar.make(requireViewBinding().recyclerView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT) }
.show()
} }
private inner class RemoteListMenuProvider : MenuProvider { private inner class RemoteListMenuProvider : MenuProvider {
@@ -106,7 +117,7 @@ class RemoteListFragment : MangaListFragment(), FilterCoordinator.Owner {
} }
R.id.action_filter_reset -> { R.id.action_filter_reset -> {
viewModel.resetFilter() filterCoordinator.reset()
true true
} }

View File

@@ -41,8 +41,6 @@ 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.exception.NotFoundException import org.koitharu.kotatsu.parsers.exception.NotFoundException
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaTag
import javax.inject.Inject import javax.inject.Inject
private const val FILTER_MIN_INTERVAL = 250L private const val FILTER_MIN_INTERVAL = 250L
@@ -51,7 +49,7 @@ private const val FILTER_MIN_INTERVAL = 250L
open class RemoteListViewModel @Inject constructor( open class RemoteListViewModel @Inject constructor(
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
mangaRepositoryFactory: MangaRepository.Factory, mangaRepositoryFactory: MangaRepository.Factory,
override val filterCoordinator: FilterCoordinator, final override val filterCoordinator: FilterCoordinator,
settings: AppSettings, settings: AppSettings,
mangaListMapper: MangaListMapper, mangaListMapper: MangaListMapper,
downloadScheduler: DownloadWorker.Scheduler, downloadScheduler: DownloadWorker.Scheduler,
@@ -132,12 +130,6 @@ open class RemoteListViewModel @Inject constructor(
} }
} }
fun resetFilter() = filterCoordinator.reset()
override fun onUpdateFilter(tags: Set<MangaTag>) {
filterCoordinator.set(MangaListFilter(tags = tags))
}
protected fun loadList(filterState: FilterCoordinator.Snapshot, append: Boolean): Job { protected fun loadList(filterState: FilterCoordinator.Snapshot, append: Boolean): Job {
loadingJob?.let { loadingJob?.let {
if (it.isActive) return it if (it.isActive) return it
@@ -178,7 +170,7 @@ open class RemoteListViewModel @Inject constructor(
icon = R.drawable.ic_empty_common, icon = R.drawable.ic_empty_common,
textPrimary = R.string.nothing_found, textPrimary = R.string.nothing_found,
textSecondary = 0, textSecondary = 0,
actionStringRes = if (canResetFilter) R.string.reset_filter else 0, actionStringRes = if (canResetFilter) R.string.reset_filter else R.string.open_in_browser,
) )
protected open suspend fun onBuildList(list: MutableList<ListModel>) = Unit protected open suspend fun onBuildList(list: MutableList<ListModel>) = Unit

View File

@@ -142,8 +142,6 @@ class MultiSearchActivity :
override fun onFilterOptionClick(option: ListFilterOption) = Unit override fun onFilterOptionClick(option: ListFilterOption) = Unit
override fun onUpdateFilter(tags: Set<MangaTag>) = Unit
override fun onFilterClick(view: View?) = Unit override fun onFilterClick(view: View?) = Unit
override fun onEmptyActionClick() = Unit override fun onEmptyActionClick() = Unit

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.suggestions.data package org.koitharu.kotatsu.suggestions.data
import android.database.DatabaseUtils.sqlEscapeString
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
@@ -48,6 +49,9 @@ abstract class SuggestionDao : MangaQueryBuilder.ConditionCallback {
@Query("SELECT tags.* FROM suggestions LEFT JOIN tags ON (tag_id IN (SELECT tag_id FROM manga_tags WHERE manga_tags.manga_id = suggestions.manga_id)) GROUP BY tag_id ORDER BY COUNT(tags.tag_id) DESC LIMIT :limit") @Query("SELECT tags.* FROM suggestions LEFT JOIN tags ON (tag_id IN (SELECT tag_id FROM manga_tags WHERE manga_tags.manga_id = suggestions.manga_id)) GROUP BY tag_id ORDER BY COUNT(tags.tag_id) DESC LIMIT :limit")
abstract suspend fun getTopTags(limit: Int): List<TagEntity> abstract suspend fun getTopTags(limit: Int): List<TagEntity>
@Query("SELECT manga.source AS count FROM suggestions LEFT JOIN manga ON manga.manga_id = suggestions.manga_id GROUP BY manga.source ORDER BY COUNT(manga.source) DESC LIMIT :limit")
abstract suspend fun getTopSources(limit: Int): List<String>
@Insert(onConflict = OnConflictStrategy.IGNORE) @Insert(onConflict = OnConflictStrategy.IGNORE)
abstract suspend fun insert(entity: SuggestionEntity): Long abstract suspend fun insert(entity: SuggestionEntity): Long
@@ -71,6 +75,7 @@ abstract class SuggestionDao : MangaQueryBuilder.ConditionCallback {
override fun getCondition(option: ListFilterOption): String? = when (option) { override fun getCondition(option: ListFilterOption): String? = when (option) {
ListFilterOption.Macro.NSFW -> "(SELECT nsfw FROM manga WHERE manga.manga_id = suggestions.manga_id) = 1" ListFilterOption.Macro.NSFW -> "(SELECT nsfw FROM manga WHERE manga.manga_id = suggestions.manga_id) = 1"
is ListFilterOption.Tag -> "EXISTS(SELECT * FROM manga_tags WHERE manga_tags.manga_id = suggestions.manga_id AND tag_id = ${option.tagId})" is ListFilterOption.Tag -> "EXISTS(SELECT * FROM manga_tags WHERE manga_tags.manga_id = suggestions.manga_id AND tag_id = ${option.tagId})"
is ListFilterOption.Source -> "(SELECT source FROM manga WHERE manga.manga_id = suggestions.manga_id) = ${sqlEscapeString(option.mangaSource.name)}"
else -> null else -> null
} }
} }

View File

@@ -8,9 +8,11 @@ import org.koitharu.kotatsu.core.db.entity.toEntity
import org.koitharu.kotatsu.core.db.entity.toManga import org.koitharu.kotatsu.core.db.entity.toManga
import org.koitharu.kotatsu.core.db.entity.toMangaTags import org.koitharu.kotatsu.core.db.entity.toMangaTags
import org.koitharu.kotatsu.core.db.entity.toMangaTagsList import org.koitharu.kotatsu.core.db.entity.toMangaTagsList
import org.koitharu.kotatsu.core.model.toMangaSources
import org.koitharu.kotatsu.core.util.ext.mapItems import org.koitharu.kotatsu.core.util.ext.mapItems
import org.koitharu.kotatsu.list.domain.ListFilterOption import org.koitharu.kotatsu.list.domain.ListFilterOption
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.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.suggestions.data.SuggestionEntity import org.koitharu.kotatsu.suggestions.data.SuggestionEntity
import javax.inject.Inject import javax.inject.Inject
@@ -56,6 +58,11 @@ class SuggestionRepository @Inject constructor(
.toMangaTagsList() .toMangaTagsList()
} }
suspend fun getTopSources(limit: Int): List<MangaSource> {
return db.getSuggestionDao().getTopSources(limit)
.toMangaSources()
}
suspend fun replace(suggestions: Iterable<MangaSuggestion>) { suspend fun replace(suggestions: Iterable<MangaSuggestion>) {
db.withTransaction { db.withTransaction {
db.getSuggestionDao().deleteAll() db.getSuggestionDao().deleteAll()

View File

@@ -1,6 +1,5 @@
package org.koitharu.kotatsu.suggestions.domain package org.koitharu.kotatsu.suggestions.domain
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.list.domain.ListFilterOption import org.koitharu.kotatsu.list.domain.ListFilterOption
import org.koitharu.kotatsu.list.domain.MangaListQuickFilter import org.koitharu.kotatsu.list.domain.MangaListQuickFilter
@@ -19,5 +18,8 @@ class SuggestionsListQuickFilter @Inject constructor(
add(ListFilterOption.Macro.NSFW) add(ListFilterOption.Macro.NSFW)
add(ListFilterOption.SFW) add(ListFilterOption.SFW)
} }
suggestionRepository.getTopSources(3).mapTo(this) {
ListFilterOption.Source(it)
}
} }
} }

View File

@@ -22,7 +22,7 @@ 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
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.databinding.FragmentFeedBinding import org.koitharu.kotatsu.databinding.FragmentListBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.list.domain.ListFilterOption import org.koitharu.kotatsu.list.domain.ListFilterOption
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
@@ -39,7 +39,7 @@ import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class FeedFragment : class FeedFragment :
BaseFragment<FragmentFeedBinding>(), BaseFragment<FragmentListBinding>(),
PaginationScrollListener.Callback, PaginationScrollListener.Callback,
MangaListListener, SwipeRefreshLayout.OnRefreshListener { MangaListListener, SwipeRefreshLayout.OnRefreshListener {
@@ -53,9 +53,9 @@ class FeedFragment :
override fun onCreateViewBinding( override fun onCreateViewBinding(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
) = FragmentFeedBinding.inflate(inflater, container, false) ) = FragmentListBinding.inflate(inflater, container, false)
override fun onViewBindingCreated(binding: FragmentFeedBinding, savedInstanceState: Bundle?) { override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState) super.onViewBindingCreated(binding, savedInstanceState)
val sizeResolver = StaticItemSizeResolver(resources.getDimensionPixelSize(R.dimen.smaller_grid_width)) val sizeResolver = StaticItemSizeResolver(resources.getDimensionPixelSize(R.dimen.smaller_grid_width))
feedAdapter = FeedAdapter(coil, viewLifecycleOwner, this, sizeResolver) { item, v -> feedAdapter = FeedAdapter(coil, viewLifecycleOwner, this, sizeResolver) { item, v ->
@@ -99,8 +99,6 @@ class FeedFragment :
override fun onRetryClick(error: Throwable) = Unit override fun onRetryClick(error: Throwable) = Unit
override fun onUpdateFilter(tags: Set<MangaTag>) = Unit
override fun onFilterClick(view: View?) = Unit override fun onFilterClick(view: View?) = Unit
override fun onEmptyActionClick() = Unit override fun onEmptyActionClick() = Unit

View File

@@ -1,27 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<org.koitharu.kotatsu.core.ui.list.fastscroll.FastScrollRecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"
android:paddingVertical="@dimen/list_spacing_normal"
app:bubbleSize="small"
app:layoutManager="org.koitharu.kotatsu.core.ui.list.FitHeightLinearLayoutManager"
tools:listitem="@layout/item_feed" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</FrameLayout>

View File

@@ -58,25 +58,33 @@
android:background="?attr/colorSecondaryContainer" android:background="?attr/colorSecondaryContainer"
android:backgroundTintMode="src_atop" android:backgroundTintMode="src_atop"
android:scaleType="centerCrop" android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="@id/guideline" app:layout_constraintBottom_toBottomOf="@id/imageView_cover2"
app:layout_constraintDimensionRatio="W,13:18" app:layout_constraintDimensionRatio="W,13:18"
app:layout_constraintStart_toStartOf="@id/guideline_start" app:layout_constraintStart_toStartOf="@id/guideline_start"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="@id/imageView_cover2"
app:layout_goneMarginTop="0dp"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small" app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
tools:src="@tools:sample/backgrounds/scenic" /> tools:src="@tools:sample/backgrounds/scenic" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier_covers"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="end"
app:constraint_referenced_ids="imageView_cover1,imageView_cover2,imageView_cover3" />
<TextView <TextView
android:id="@+id/textView_title" android:id="@+id/textView_title"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="0dp"
android:layout_marginStart="@dimen/margin_normal" android:layout_marginStart="@dimen/margin_normal"
android:layout_marginEnd="?listPreferredItemPaddingEnd" android:layout_marginEnd="?listPreferredItemPaddingEnd"
android:ellipsize="end" android:ellipsize="end"
android:gravity="center_vertical|start"
android:textAppearance="?attr/textAppearanceBodyLarge" android:textAppearance="?attr/textAppearanceBodyLarge"
app:layout_constrainedHeight="true"
app:layout_constraintBottom_toBottomOf="@id/guideline" app:layout_constraintBottom_toBottomOf="@id/guideline"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/imageView_cover3" app:layout_constraintStart_toEndOf="@id/barrier_covers"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/lorem[22]" /> tools:text="@tools:sample/lorem[22]" />

View File

@@ -325,7 +325,7 @@
<string name="manga_error_description_pattern">Error details:&lt;br&gt;&lt;tt&gt;%1$s&lt;/tt&gt;&lt;br&gt;&lt;br&gt;1. Try to &lt;a href="%2$s"&gt;open manga in a web browser&lt;/a&gt; to ensure it is available on its source&lt;br&gt;2. Make sure you are using the &lt;a href="kotatsu://about"&gt;latest version of Kotatsu&lt;/a&gt;&lt;br&gt;3. If it is available, send an error report to the developers.</string> <string name="manga_error_description_pattern">Error details:&lt;br&gt;&lt;tt&gt;%1$s&lt;/tt&gt;&lt;br&gt;&lt;br&gt;1. Try to &lt;a href="%2$s"&gt;open manga in a web browser&lt;/a&gt; to ensure it is available on its source&lt;br&gt;2. Make sure you are using the &lt;a href="kotatsu://about"&gt;latest version of Kotatsu&lt;/a&gt;&lt;br&gt;3. If it is available, send an error report to the developers.</string>
<string name="history_shortcuts">Show recent manga shortcuts</string> <string name="history_shortcuts">Show recent manga shortcuts</string>
<string name="history_shortcuts_summary">Make recent manga available by long pressing on application icon</string> <string name="history_shortcuts_summary">Make recent manga available by long pressing on application icon</string>
<string name="reader_control_ltr_summary">Tapping on the right edge, or pressing the right key, always switches to the next page.</string> <string name="reader_control_ltr_summary">Do not adjust the page switching direction to the reader mode, e. g. pressing the right key always switches to the next page. This option affects only hardware input devices</string>
<string name="reader_control_ltr">Ergonomic reader control</string> <string name="reader_control_ltr">Ergonomic reader control</string>
<string name="color_correction">Color correction</string> <string name="color_correction">Color correction</string>
<string name="brightness">Brightness</string> <string name="brightness">Brightness</string>