From 6d84294533a27495b4cc5eea423594003b389a50 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 23 Sep 2024 14:36:29 +0300 Subject: [PATCH] Improve quick filters --- .../kotatsu/core/model/MangaSource.kt | 22 ++++-- .../core/parser/favicon/FaviconFetcher.kt | 1 + .../kotatsu/core/ui/widgets/ChipsView.kt | 76 +++++++++++++++++-- .../koitharu/kotatsu/core/util/ext/String.kt | 9 +++ .../kotatsu/favourites/data/FavouritesDao.kt | 8 ++ .../domain/FavoritesListQuickFilter.kt | 16 +++- .../favourites/domain/FavouritesRepository.kt | 13 ++++ .../select/adapter/CategoriesHeaderAD.kt | 24 ++++-- .../ui/list/FavouritesListViewModel.kt | 11 ++- .../kotatsu/filter/ui/FilterCoordinator.kt | 2 +- .../kotatsu/history/data/HistoryDao.kt | 5 ++ .../kotatsu/history/data/HistoryRepository.kt | 10 ++- .../history/domain/HistoryListQuickFilter.kt | 3 + .../kotatsu/list/domain/ListFilterOption.kt | 33 ++++++++ .../list/domain/MangaListQuickFilter.kt | 1 + .../kotatsu/list/ui/MangaListFragment.kt | 4 - .../kotatsu/list/ui/MangaListViewModel.kt | 2 - .../list/ui/adapter/MangaListListener.kt | 3 - .../kotatsu/list/ui/model/ListHeader.kt | 2 +- .../local/data/index/LocalMangaIndex.kt | 21 +++-- .../kotatsu/local/ui/LocalListViewModel.kt | 2 +- .../remotelist/ui/RemoteListFragment.kt | 23 ++++-- .../remotelist/ui/RemoteListViewModel.kt | 12 +-- .../search/ui/multi/MultiSearchActivity.kt | 2 - .../kotatsu/suggestions/data/SuggestionDao.kt | 5 ++ .../domain/SuggestionRepository.kt | 7 ++ .../domain/SuggestionsListQuickFilter.kt | 4 +- .../kotatsu/tracker/ui/feed/FeedFragment.kt | 10 +-- app/src/main/res/layout/fragment_feed.xml | 27 ------- .../res/layout/item_categories_header.xml | 18 +++-- app/src/main/res/values/strings.xml | 2 +- 31 files changed, 275 insertions(+), 103 deletions(-) delete mode 100644 app/src/main/res/layout/fragment_feed.xml diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/MangaSource.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/MangaSource.kt index 66c348689..84c12973a 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/MangaSource.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/MangaSource.kt @@ -43,6 +43,8 @@ fun MangaSource(name: String?): MangaSource { return UnknownMangaSource } +fun Collection.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) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/favicon/FaviconFetcher.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/favicon/FaviconFetcher.kt index ef155b03e..035e14c9e 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/favicon/FaviconFetcher.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/favicon/FaviconFetcher.kt @@ -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, it.value) } val response = okHttpClient.newCall(request.build()).await() diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/ChipsView.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/ChipsView.kt index bb0f7972d..d22681ecb 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/ChipsView.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/ChipsView.kt @@ -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 { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/String.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/String.kt index 92d77b6a1..1b4917bd4 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/String.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/String.kt @@ -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 Collection.joinToStringWithLimit(context: Context, limit: Int, transf } } } + +@Deprecated("", + ReplaceWith( + "sqlEscapeString(this)", + "android.database.DatabaseUtils.sqlEscapeString" + ) +) +fun String.sqlEscape(): String = DatabaseUtils.sqlEscapeString(this) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt index fe62f0334..8e0443d6d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt @@ -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 + + @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 + /** 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 } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/FavoritesListQuickFilter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/FavoritesListQuickFilter.kt index d2de06707..24167296c 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/FavoritesListQuickFilter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/FavoritesListQuickFilter.kt @@ -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 } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt index 1e0d91693..eed5c14e7 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt @@ -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 { + return db.getFavouritesDao().run { + if (categoryId == 0L) { + findPopularSources(limit) + } else { + findPopularSources(categoryId, limit) + } + }.toMangaSources() + } + suspend fun createCategory( title: String, sortOrder: ListSortOrder, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/adapter/CategoriesHeaderAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/adapter/CategoriesHeaderAD.kt index 60872f802..28039b358 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/adapter/CategoriesHeaderAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/select/adapter/CategoriesHeaderAD.kt @@ -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) + } } } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt index 18863085e..fd559caa3 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt @@ -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) { launchLoadingJob(Dispatchers.Default) { markAsReadUseCase(items) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt index 0bdbae1f4..248760075 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt @@ -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 = currentListFilter.map { it.query } .stateIn(coroutineScope, SharingStarted.Eagerly, null) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryDao.kt index 23e0db88e..c0c9b9bd0 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryDao.kt @@ -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 + @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 + @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 } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt index 6e323bd32..6ffe83458 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/data/HistoryRepository.kt @@ -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 { - return db.getHistoryDao().findPopularTags(limit).map { x -> x.toMangaTag() } + return db.getHistoryDao().findPopularTags(limit).toMangaTagsList() + } + + suspend fun getPopularSources(limit: Int): List { + return db.getHistoryDao().findPopularSources(limit).toMangaSources() } fun shouldSkip(manga: Manga): Boolean { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/history/domain/HistoryListQuickFilter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/domain/HistoryListQuickFilter.kt index e05db894d..bf0ae0ccf 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/history/domain/HistoryListQuickFilter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/domain/HistoryListQuickFilter.kt @@ -31,5 +31,8 @@ class HistoryListQuickFilter @Inject constructor( repository.getPopularTags(3).mapTo(this) { ListFilterOption.Tag(it) } + repository.getPopularSources(3).mapTo(this) { + ListFilterOption.Source(it) + } } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/ListFilterOption.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/ListFilterOption.kt index 83b4107e4..71f6a4cf0 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/ListFilterOption.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/ListFilterOption.kt @@ -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, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/MangaListQuickFilter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/MangaListQuickFilter.kt index 9b224cb2f..8568c4c20 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/MangaListQuickFilter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/MangaListQuickFilter.kt @@ -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, ) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListFragment.kt index 9184b0f8a..e7d48b33b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListFragment.kt @@ -251,10 +251,6 @@ abstract class MangaListFragment : resolveException(error) } - override fun onUpdateFilter(tags: Set) { - viewModel.onUpdateFilter(tags) - } - private fun onGridScaleChanged(scale: Float) { spanSizeLookup.invalidateCache() spanResolver?.setGridSize(scale, requireViewBinding().recyclerView) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt index dae93587b..5278dbf56 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt @@ -43,8 +43,6 @@ abstract class MangaListViewModel( val isIncognitoModeEnabled: Boolean get() = settings.isIncognitoModeEnabled - open fun onUpdateFilter(tags: Set) = Unit - abstract fun onRefresh() abstract fun onRetry() diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListListener.kt index 609126836..6cd00704d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListListener.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListListener.kt @@ -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) - fun onFilterClick(view: View?) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/ListHeader.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/ListHeader.kt index 3608e550d..72c78c0ac 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/ListHeader.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/ListHeader.kt @@ -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, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/index/LocalMangaIndex.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/index/LocalMangaIndex.kt index 9a9c08f43..f954f78b5 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/index/LocalMangaIndex.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/index/LocalMangaIndex.kt @@ -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 { 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) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt index 08217f7fa..5c60c1bf9 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt @@ -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, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt index 5776e1444..19f3aed74 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt @@ -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 } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt index 3727776b1..c3b948f18 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt @@ -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) { - 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) = Unit diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchActivity.kt index a719c4f20..f4975fa04 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchActivity.kt @@ -142,8 +142,6 @@ class MultiSearchActivity : override fun onFilterOptionClick(option: ListFilterOption) = Unit - override fun onUpdateFilter(tags: Set) = Unit - override fun onFilterClick(view: View?) = Unit override fun onEmptyActionClick() = Unit diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/data/SuggestionDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/data/SuggestionDao.kt index 317f2ffff..f61d88f3f 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/data/SuggestionDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/data/SuggestionDao.kt @@ -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 + @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 + @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 } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/domain/SuggestionRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/domain/SuggestionRepository.kt index 7de9f3482..b7f8a4e6a 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/domain/SuggestionRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/domain/SuggestionRepository.kt @@ -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 { + return db.getSuggestionDao().getTopSources(limit) + .toMangaSources() + } + suspend fun replace(suggestions: Iterable) { db.withTransaction { db.getSuggestionDao().deleteAll() diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/domain/SuggestionsListQuickFilter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/domain/SuggestionsListQuickFilter.kt index 78b7ea86c..18c69deb6 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/domain/SuggestionsListQuickFilter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/domain/SuggestionsListQuickFilter.kt @@ -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) + } } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedFragment.kt index 935bbe0db..4643610f1 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedFragment.kt @@ -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(), + BaseFragment(), 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) = Unit - override fun onFilterClick(view: View?) = Unit override fun onEmptyActionClick() = Unit diff --git a/app/src/main/res/layout/fragment_feed.xml b/app/src/main/res/layout/fragment_feed.xml deleted file mode 100644 index 54bbad473..000000000 --- a/app/src/main/res/layout/fragment_feed.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - diff --git a/app/src/main/res/layout/item_categories_header.xml b/app/src/main/res/layout/item_categories_header.xml index dc176e98a..97990dd41 100644 --- a/app/src/main/res/layout/item_categories_header.xml +++ b/app/src/main/res/layout/item_categories_header.xml @@ -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" /> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6f78da58e..c49fe7fd5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -325,7 +325,7 @@ Error details:<br><tt>%1$s</tt><br><br>1. Try to <a href="%2$s">open manga in a web browser</a> to ensure it is available on its source<br>2. Make sure you are using the <a href="kotatsu://about">latest version of Kotatsu</a><br>3. If it is available, send an error report to the developers. Show recent manga shortcuts Make recent manga available by long pressing on application icon - Tapping on the right edge, or pressing the right key, always switches to the next page. + 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 Ergonomic reader control Color correction Brightness