Improve quick filters

master
Koitharu 2 years ago committed by Mac135135
parent 36bd3cc438
commit 6d84294533

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

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

@ -8,18 +8,32 @@ import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
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.ChipDrawable
import com.google.android.material.chip.ChipGroup
import dagger.hilt.android.AndroidEntryPoint
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
@AndroidEntryPoint
class ChipsView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = com.google.android.material.R.attr.chipGroupStyle,
) : ChipGroup(context, attrs, defStyleAttr) {
@Inject
lateinit var coil: ImageLoader
private var isLayoutSuppressedCompat = false
private var isLayoutCalledOnSuppressed = false
private val chipOnClickListener = InternalChipClickListener()
@ -90,8 +104,10 @@ class ChipsView @JvmOverloads constructor(
val title: CharSequence? = null,
@StringRes val titleResId: Int = 0,
@DrawableRes val icon: Int = 0,
val iconData: Any? = null,
@ColorRes val tint: Int = 0,
val isChecked: Boolean = false,
val isLoading: Boolean = false,
val isDropdown: Boolean = false,
val isCloseable: Boolean = false,
val data: Any? = null,
@ -100,6 +116,7 @@ class ChipsView @JvmOverloads constructor(
private inner class DataChip(context: Context) : Chip(context) {
private var model: ChipModel? = null
private var imageRequest: Disposable? = null
init {
val drawable = ChipDrawable.createFromAttributes(context, null, 0, chipStyle)
@ -112,6 +129,9 @@ class ChipsView @JvmOverloads constructor(
}
fun bind(model: ChipModel) {
if (this.model == model) {
return
}
this.model = model
if (model.titleResId == 0) {
@ -127,13 +147,7 @@ class ChipsView @JvmOverloads constructor(
isChecked = false
isCheckable = false
}
if (model.icon == 0 || model.isChecked) {
chipIcon = null
isChipIconVisible = false
} else {
setChipIconResource(model.icon)
isChipIconVisible = true
}
bindIcon(model)
isCheckedIconVisible = model.isChecked
isCloseIconVisible = if (model.isCloseable || model.isDropdown) {
setCloseIconResource(
@ -147,6 +161,54 @@ class ChipsView @JvmOverloads constructor(
}
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 {

@ -1,6 +1,7 @@
package org.koitharu.kotatsu.core.util.ext
import android.content.Context
import android.database.DatabaseUtils
import androidx.annotation.FloatRange
import org.koitharu.kotatsu.R
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)

@ -1,5 +1,6 @@
package org.koitharu.kotatsu.favourites.data
import android.database.DatabaseUtils.sqlEscapeString
import androidx.room.Dao
import androidx.room.Insert
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")
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(onConflict = OnConflictStrategy.REPLACE)
@ -200,6 +207,7 @@ abstract class FavouritesDao : MangaQueryBuilder.ConditionCallback {
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})"
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
}
}

@ -1,12 +1,15 @@
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.prefs.AppSettings
import org.koitharu.kotatsu.list.domain.ListFilterOption
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 repository: FavouritesRepository,
networkState: NetworkState,
@ -22,5 +25,14 @@ class FavoritesListQuickFilter @Inject constructor(
add(ListFilterOption.Macro.NEW_CHAPTERS)
}
add(ListFilterOption.Macro.COMPLETED)
repository.findPopularSources(categoryId, 3).mapTo(this) {
ListFilterOption.Source(it)
}
}
@AssistedFactory
interface Factory {
fun create(categoryId: Long): FavoritesListQuickFilter
}
}

@ -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.toEntity
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.util.ext.mapItems
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.ListSortOrder
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import javax.inject.Inject
@Reusable
@ -136,6 +139,16 @@ class FavouritesRepository @Inject constructor(
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(
title: String,
sortOrder: ListSortOrder,

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

@ -49,12 +49,13 @@ class FavouritesListViewModel @Inject constructor(
private val repository: FavouritesRepository,
private val mangaListMapper: MangaListMapper,
private val markAsReadUseCase: MarkAsReadUseCase,
private val quickFilter: FavoritesListQuickFilter,
quickFilterFactory: FavoritesListQuickFilter.Factory,
settings: AppSettings,
downloadScheduler: DownloadWorker.Scheduler,
) : MangaListViewModel(settings, downloadScheduler), QuickFilterListener by quickFilter {
) : MangaListViewModel(settings, downloadScheduler), QuickFilterListener {
val categoryId: Long = savedStateHandle[ARG_CATEGORY_ID] ?: NO_ID
private val quickFilter = quickFilterFactory.create(categoryId)
private val refreshTrigger = MutableStateFlow(Any())
private val limit = MutableStateFlow(PAGE_SIZE)
private val isPaginationReady = AtomicBoolean(false)
@ -91,6 +92,12 @@ class FavouritesListViewModel @Inject constructor(
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>) {
launchLoadingJob(Dispatchers.Default) {
markAsReadUseCase(items)

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

@ -1,5 +1,6 @@
package org.koitharu.kotatsu.history.data
import android.database.DatabaseUtils.sqlEscapeString
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
@ -73,6 +74,9 @@ abstract class HistoryDao : MangaQueryBuilder.ConditionCallback {
)
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")
abstract suspend fun find(id: Long): HistoryEntity?
@ -160,6 +164,7 @@ abstract class HistoryDao : MangaQueryBuilder.ConditionCallback {
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})"
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
}
}

@ -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.toMangaTag
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.MangaSource
import org.koitharu.kotatsu.core.model.findById
import org.koitharu.kotatsu.core.model.isLocal
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.prefs.AppSettings
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.ReadingProgress
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.scrobbling.common.domain.Scrobbler
import org.koitharu.kotatsu.scrobbling.common.domain.tryScrobble
@ -177,7 +181,11 @@ class HistoryRepository @Inject constructor(
}
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 {

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

@ -5,6 +5,12 @@ import androidx.annotation.StringRes
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.db.entity.toEntity
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
sealed interface ListFilterOption {
@ -19,6 +25,8 @@ sealed interface ListFilterOption {
val groupKey: String
fun getIconData(): Any? = null
data object Downloaded : ListFilterOption {
override val titleResId: Int
@ -88,6 +96,31 @@ sealed interface ListFilterOption {
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(
val option: ListFilterOption,
override val iconResId: Int,

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

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

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

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

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

@ -7,6 +7,8 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.FlowCollector
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.parser.MangaDataRepository
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
@ -30,6 +32,7 @@ class LocalMangaIndex @Inject constructor(
) : FlowCollector<LocalManga?> {
private val prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
private val mutex = Mutex()
private var previousHash: Long
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()
if (newHash == previousHash) {
return false
@ -57,7 +60,13 @@ class LocalMangaIndex @Inject constructor(
}
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 {
LocalMangaInput.of(File(path)).getManga()
}.onFailure {
@ -65,9 +74,11 @@ class LocalMangaIndex @Inject constructor(
}.getOrNull()
}
suspend fun put(manga: LocalManga) = db.withTransaction {
mangaDataRepository.storeManga(manga.manga)
db.getLocalMangaIndexDao().upsert(manga.toEntity())
suspend fun put(manga: LocalManga) = mutex.withLock {
db.withTransaction {
mangaDataRepository.storeManga(manga.manga)
db.getLocalMangaIndexDao().upsert(manga.toEntity())
}
}
suspend fun delete(mangaId: Long) {

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

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

@ -41,8 +41,6 @@ import org.koitharu.kotatsu.list.ui.model.toErrorFooter
import org.koitharu.kotatsu.list.ui.model.toErrorState
import org.koitharu.kotatsu.parsers.exception.NotFoundException
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
private const val FILTER_MIN_INTERVAL = 250L
@ -51,7 +49,7 @@ private const val FILTER_MIN_INTERVAL = 250L
open class RemoteListViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
mangaRepositoryFactory: MangaRepository.Factory,
override val filterCoordinator: FilterCoordinator,
final override val filterCoordinator: FilterCoordinator,
settings: AppSettings,
mangaListMapper: MangaListMapper,
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 {
loadingJob?.let {
if (it.isActive) return it
@ -178,7 +170,7 @@ open class RemoteListViewModel @Inject constructor(
icon = R.drawable.ic_empty_common,
textPrimary = R.string.nothing_found,
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

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

@ -1,5 +1,6 @@
package org.koitharu.kotatsu.suggestions.data
import android.database.DatabaseUtils.sqlEscapeString
import androidx.room.Dao
import androidx.room.Insert
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")
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)
abstract suspend fun insert(entity: SuggestionEntity): Long
@ -71,6 +75,7 @@ abstract class SuggestionDao : MangaQueryBuilder.ConditionCallback {
override fun getCondition(option: ListFilterOption): String? = when (option) {
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.Source -> "(SELECT source FROM manga WHERE manga.manga_id = suggestions.manga_id) = ${sqlEscapeString(option.mangaSource.name)}"
else -> null
}
}

@ -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.toMangaTags
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.list.domain.ListFilterOption
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.suggestions.data.SuggestionEntity
import javax.inject.Inject
@ -56,6 +58,11 @@ class SuggestionRepository @Inject constructor(
.toMangaTagsList()
}
suspend fun getTopSources(limit: Int): List<MangaSource> {
return db.getSuggestionDao().getTopSources(limit)
.toMangaSources()
}
suspend fun replace(suggestions: Iterable<MangaSuggestion>) {
db.withTransaction {
db.getSuggestionDao().deleteAll()

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

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

@ -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>

@ -58,25 +58,33 @@
android:background="?attr/colorSecondaryContainer"
android:backgroundTintMode="src_atop"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="@id/guideline"
app:layout_constraintBottom_toBottomOf="@id/imageView_cover2"
app:layout_constraintDimensionRatio="W,13:18"
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"
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
android:id="@+id/textView_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_height="0dp"
android:layout_marginStart="@dimen/margin_normal"
android:layout_marginEnd="?listPreferredItemPaddingEnd"
android:ellipsize="end"
android:gravity="center_vertical|start"
android:textAppearance="?attr/textAppearanceBodyLarge"
app:layout_constrainedHeight="true"
app:layout_constraintBottom_toBottomOf="@id/guideline"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/imageView_cover3"
app:layout_constraintStart_toEndOf="@id/barrier_covers"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/lorem[22]" />

@ -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="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="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="color_correction">Color correction</string>
<string name="brightness">Brightness</string>

Loading…
Cancel
Save