From 20852dbd12967f969465cbbdf379f3e38ceb4a38 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Thu, 1 Aug 2024 20:34:25 +0300 Subject: [PATCH 01/24] Fix query plugin source capabilities --- .../exceptions/IncompatiblePluginException.kt | 6 + .../external/ExternalMangaRepository.kt | 236 ++------------ .../external/ExternalPluginContentSource.kt | 291 ++++++++++++++++++ .../core/parser/external/IndexedCursor.kt | 80 +++++ .../koitharu/kotatsu/core/util/ext/Cursor.kt | 2 + .../kotatsu/core/util/ext/Throwable.kt | 3 +- app/src/main/res/values/strings.xml | 1 + 7 files changed, 408 insertions(+), 211 deletions(-) create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/IncompatiblePluginException.kt create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/ExternalPluginContentSource.kt create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/IndexedCursor.kt diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/IncompatiblePluginException.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/IncompatiblePluginException.kt new file mode 100644 index 000000000..f22b74989 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/IncompatiblePluginException.kt @@ -0,0 +1,6 @@ +package org.koitharu.kotatsu.core.exceptions + +class IncompatiblePluginException( + val name: String?, + cause: Throwable?, +) : RuntimeException(cause) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/ExternalMangaRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/ExternalMangaRepository.kt index a047d0ebc..c888495e4 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/ExternalMangaRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/ExternalMangaRepository.kt @@ -1,19 +1,12 @@ package org.koitharu.kotatsu.core.parser.external import android.content.ContentResolver -import android.database.Cursor -import androidx.collection.ArraySet -import androidx.core.database.getStringOrNull -import androidx.core.net.toUri import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.runInterruptible import org.koitharu.kotatsu.core.cache.MemoryContentCache import org.koitharu.kotatsu.core.parser.CachingMangaRepository -import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.parsers.model.ContentRating -import org.koitharu.kotatsu.parsers.model.ContentType import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaListFilter @@ -21,9 +14,6 @@ import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaState import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.SortOrder -import org.koitharu.kotatsu.parsers.util.find -import org.koitharu.kotatsu.parsers.util.mapNotNullToSet -import org.koitharu.kotatsu.parsers.util.splitTwoParts import java.util.EnumSet import java.util.Locale @@ -33,232 +23,58 @@ class ExternalMangaRepository( cache: MemoryContentCache, ) : CachingMangaRepository(cache) { - private val capabilities by lazy { queryCapabilities() } + private val contentSource = ExternalPluginContentSource(contentResolver, source) + + private val capabilities by lazy { + runCatching { + contentSource.getCapabilities() + }.onFailure { + it.printStackTraceDebug() + }.getOrNull() + } override val sortOrders: Set get() = capabilities?.availableSortOrders ?: EnumSet.of(SortOrder.ALPHABETICAL) + override val states: Set get() = capabilities?.availableStates.orEmpty() + override val contentRatings: Set get() = capabilities?.availableContentRating.orEmpty() + override var defaultSortOrder: SortOrder get() = capabilities?.defaultSortOrder ?: SortOrder.ALPHABETICAL set(value) = Unit + override val isMultipleTagsSupported: Boolean get() = capabilities?.isMultipleTagsSupported ?: true + override val isTagsExclusionSupported: Boolean get() = capabilities?.isTagsExclusionSupported ?: false + override val isSearchSupported: Boolean get() = capabilities?.isSearchSupported ?: true override suspend fun getList(offset: Int, filter: MangaListFilter?): List = - runInterruptible(Dispatchers.Default) { - val uri = "content://${source.authority}/manga".toUri().buildUpon() - uri.appendQueryParameter("offset", offset.toString()) - when (filter) { - is MangaListFilter.Advanced -> { - filter.tags.forEach { uri.appendQueryParameter("tag_include", it.key) } - filter.tagsExclude.forEach { uri.appendQueryParameter("tag_exclude", it.key) } - filter.states.forEach { uri.appendQueryParameter("state", it.name) } - filter.locale?.let { uri.appendQueryParameter("locale", it.language) } - filter.contentRating.forEach { uri.appendQueryParameter("content_rating", it.name) } - } - - is MangaListFilter.Search -> { - uri.appendQueryParameter("query", filter.query) - } - - null -> Unit - } - contentResolver.query(uri.build(), null, null, null, filter?.sortOrder?.name)?.use { cursor -> - val result = ArrayList(cursor.count) - if (cursor.moveToFirst()) { - do { - result += cursor.getManga() - } while (cursor.moveToNext()) - } - result - }.orEmpty() + runInterruptible(Dispatchers.IO) { + contentSource.getList(offset, filter) } - override suspend fun getDetailsImpl(manga: Manga): Manga = coroutineScope { - val chapters = async { queryChapters(manga.url) } - val details = queryDetails(manga.url) - Manga( - id = manga.id, - title = details.title.ifBlank { manga.title }, - altTitle = details.altTitle.ifNullOrEmpty { manga.altTitle }, - url = details.url.ifEmpty { manga.url }, - publicUrl = details.publicUrl.ifEmpty { manga.publicUrl }, - rating = maxOf(details.rating, manga.rating), - isNsfw = details.isNsfw, - coverUrl = details.coverUrl.ifEmpty { manga.coverUrl }, - tags = details.tags + manga.tags, - state = details.state ?: manga.state, - author = details.author.ifNullOrEmpty { manga.author }, - largeCoverUrl = details.largeCoverUrl.ifNullOrEmpty { manga.largeCoverUrl }, - description = details.description.ifNullOrEmpty { manga.description }, - chapters = chapters.await(), - source = source, - ) + override suspend fun getDetailsImpl(manga: Manga): Manga = runInterruptible(Dispatchers.IO) { + contentSource.getDetails(manga) } - override suspend fun getPagesImpl(chapter: MangaChapter): List = runInterruptible(Dispatchers.Default) { - val uri = "content://${source.authority}/chapters".toUri() - .buildUpon() - .appendPath(chapter.url) - .build() - contentResolver.query(uri, null, null, null, null)?.use { cursor -> - val result = ArrayList(cursor.count) - if (cursor.moveToFirst()) { - do { - result += MangaPage( - id = cursor.getLong(0), - url = cursor.getString(1), - preview = cursor.getStringOrNull(2), - source = source, - ) - } while (cursor.moveToNext()) - } - result - }.orEmpty() + override suspend fun getPagesImpl(chapter: MangaChapter): List = runInterruptible(Dispatchers.IO) { + contentSource.getPages(chapter) } - override suspend fun getPageUrl(page: MangaPage): String = page.url + override suspend fun getPageUrl(page: MangaPage): String = page.url // TODO - override suspend fun getTags(): Set = runInterruptible(Dispatchers.Default) { - val uri = "content://${source.authority}/tags".toUri() - contentResolver.query(uri, null, null, null, null)?.use { cursor -> - val result = ArraySet(cursor.count) - if (cursor.moveToFirst()) { - do { - result += MangaTag( - key = cursor.getString(0), - title = cursor.getString(1), - source = source, - ) - } while (cursor.moveToNext()) - } - result - }.orEmpty() + override suspend fun getTags(): Set = runInterruptible(Dispatchers.IO) { + contentSource.getTags() } - override suspend fun getLocales(): Set = emptySet() + override suspend fun getLocales(): Set = emptySet() // TODO override suspend fun getRelatedMangaImpl(seed: Manga): List = emptyList() // TODO - - private suspend fun queryDetails(url: String): Manga = runInterruptible(Dispatchers.Default) { - val uri = "content://${source.authority}/manga".toUri() - .buildUpon() - .appendPath(url) - .build() - checkNotNull( - contentResolver.query(uri, null, null, null, null)?.use { cursor -> - cursor.moveToFirst() - cursor.getManga() - }, - ) - } - - private suspend fun queryChapters(url: String): List? = runInterruptible(Dispatchers.Default) { - val uri = "content://${source.authority}/manga/chapters".toUri() - .buildUpon() - .appendPath(url) - .build() - contentResolver.query(uri, null, null, null, null)?.use { cursor -> - val result = ArrayList(cursor.count) - if (cursor.moveToFirst()) { - do { - result += MangaChapter( - id = cursor.getLong(0), - name = cursor.getString(1), - number = cursor.getFloat(2), - volume = cursor.getInt(3), - url = cursor.getString(4), - scanlator = cursor.getStringOrNull(5), - uploadDate = cursor.getLong(6), - branch = cursor.getStringOrNull(7), - source = source, - ) - } while (cursor.moveToNext()) - } - result - } - } - - private fun Cursor.getManga() = Manga( - id = getLong(0), - title = getString(1), - altTitle = getStringOrNull(2), - url = getString(3), - publicUrl = getString(4), - rating = getFloat(5), - isNsfw = getInt(6) > 1, - coverUrl = getString(7), - tags = getStringOrNull(8)?.split(':')?.mapNotNullToSet { - val parts = it.splitTwoParts('=') ?: return@mapNotNullToSet null - MangaTag(key = parts.first, title = parts.second, source = source) - }.orEmpty(), - state = getStringOrNull(9)?.let { MangaState.entries.find(it) }, - author = optString(10), - largeCoverUrl = optString(11), - description = optString(12), - chapters = emptyList(), - source = source, - ) - - private fun Cursor.optString(columnIndex: Int): String? { - return if (isNull(columnIndex)) { - null - } else { - getString(columnIndex) - } - } - - private fun queryCapabilities(): MangaSourceCapabilities? { - val uri = "content://${source.authority}/capabilities".toUri() - return contentResolver.query(uri, null, null, null, null)?.use { cursor -> - if (cursor.moveToFirst()) { - MangaSourceCapabilities( - availableSortOrders = cursor.getStringOrNull(0) - ?.split(',') - ?.mapNotNullTo(EnumSet.noneOf(SortOrder::class.java)) { - SortOrder.entries.find(it) - }.orEmpty(), - availableStates = cursor.getStringOrNull(1) - ?.split(',') - ?.mapNotNullTo(EnumSet.noneOf(MangaState::class.java)) { - MangaState.entries.find(it) - }.orEmpty(), - availableContentRating = cursor.getStringOrNull(2) - ?.split(',') - ?.mapNotNullTo(EnumSet.noneOf(ContentRating::class.java)) { - ContentRating.entries.find(it) - }.orEmpty(), - isMultipleTagsSupported = cursor.getInt(3) > 1, - isTagsExclusionSupported = cursor.getInt(4) > 1, - isSearchSupported = cursor.getInt(5) > 1, - contentType = ContentType.entries.find(cursor.getString(6)) ?: ContentType.OTHER, - defaultSortOrder = cursor.getStringOrNull(7)?.let { - SortOrder.entries.find(it) - } ?: SortOrder.ALPHABETICAL, - sourceLocale = cursor.getStringOrNull(8)?.let { Locale(it) } ?: Locale.ROOT, - ) - } else { - null - } - } - } - - private class MangaSourceCapabilities( - val availableSortOrders: Set, - val availableStates: Set, - val availableContentRating: Set, - val isMultipleTagsSupported: Boolean, - val isTagsExclusionSupported: Boolean, - val isSearchSupported: Boolean, - val contentType: ContentType, - val defaultSortOrder: SortOrder, - val sourceLocale: Locale, - ) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/ExternalPluginContentSource.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/ExternalPluginContentSource.kt new file mode 100644 index 000000000..5e7d9a800 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/ExternalPluginContentSource.kt @@ -0,0 +1,291 @@ +package org.koitharu.kotatsu.core.parser.external + +import android.content.ContentResolver +import android.database.Cursor +import androidx.annotation.WorkerThread +import androidx.collection.ArraySet +import androidx.core.net.toUri +import org.jetbrains.annotations.Blocking +import org.koitharu.kotatsu.core.exceptions.IncompatiblePluginException +import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty +import org.koitharu.kotatsu.core.util.ext.toLocale +import org.koitharu.kotatsu.parsers.model.ContentRating +import org.koitharu.kotatsu.parsers.model.ContentType +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaChapter +import org.koitharu.kotatsu.parsers.model.MangaListFilter +import org.koitharu.kotatsu.parsers.model.MangaPage +import org.koitharu.kotatsu.parsers.model.MangaState +import org.koitharu.kotatsu.parsers.model.MangaTag +import org.koitharu.kotatsu.parsers.model.SortOrder +import org.koitharu.kotatsu.parsers.util.find +import org.koitharu.kotatsu.parsers.util.mapNotNullToSet +import org.koitharu.kotatsu.parsers.util.splitTwoParts +import java.util.EnumSet +import java.util.Locale + +class ExternalPluginContentSource( + private val contentResolver: ContentResolver, + private val source: ExternalMangaSource, +) { + + @Blocking + @WorkerThread + fun getList(offset: Int, filter: MangaListFilter?): List = runCatchingCompatibility { + val uri = "content://${source.authority}/manga".toUri().buildUpon() + uri.appendQueryParameter("offset", offset.toString()) + when (filter) { + is MangaListFilter.Advanced -> { + filter.tags.forEach { uri.appendQueryParameter("tag_include", it.key) } + filter.tagsExclude.forEach { uri.appendQueryParameter("tag_exclude", it.key) } + filter.states.forEach { uri.appendQueryParameter("state", it.name) } + filter.locale?.let { uri.appendQueryParameter("locale", it.language) } + filter.contentRating.forEach { uri.appendQueryParameter("content_rating", it.name) } + } + + is MangaListFilter.Search -> { + uri.appendQueryParameter("query", filter.query) + } + + null -> Unit + } + contentResolver.query(uri.build(), null, null, null, filter?.sortOrder?.name) + .indexed() + .use { cursor -> + val result = ArrayList(cursor.count) + if (cursor.moveToFirst()) { + do { + result += cursor.getManga() + } while (cursor.moveToNext()) + } + result + } + } + + @Blocking + @WorkerThread + fun getDetails(manga: Manga) = runCatchingCompatibility { + val chapters = queryChapters(manga.url) + val details = queryDetails(manga.url) + Manga( + id = manga.id, + title = details.title.ifBlank { manga.title }, + altTitle = details.altTitle.ifNullOrEmpty { manga.altTitle }, + url = details.url.ifEmpty { manga.url }, + publicUrl = details.publicUrl.ifEmpty { manga.publicUrl }, + rating = maxOf(details.rating, manga.rating), + isNsfw = details.isNsfw, + coverUrl = details.coverUrl.ifEmpty { manga.coverUrl }, + tags = details.tags + manga.tags, + state = details.state ?: manga.state, + author = details.author.ifNullOrEmpty { manga.author }, + largeCoverUrl = details.largeCoverUrl.ifNullOrEmpty { manga.largeCoverUrl }, + description = details.description.ifNullOrEmpty { manga.description }, + chapters = chapters, + source = source, + ) + } + + @Blocking + @WorkerThread + fun getPages(chapter: MangaChapter): List = runCatchingCompatibility { + val uri = "content://${source.authority}/chapters".toUri() + .buildUpon() + .appendPath(chapter.url) + .build() + contentResolver.query(uri, null, null, null, null) + .indexed() + .use { cursor -> + val result = ArrayList(cursor.count) + if (cursor.moveToFirst()) { + do { + result += MangaPage( + id = cursor.getLong(COLUMN_ID), + url = cursor.getString(COLUMN_URL), + preview = cursor.getStringOrNull(COLUMN_PREVIEW), + source = source, + ) + } while (cursor.moveToNext()) + } + result + } + } + + @Blocking + @WorkerThread + fun getTags(): Set = runCatchingCompatibility { + val uri = "content://${source.authority}/tags".toUri() + contentResolver.query(uri, null, null, null, null) + .indexed() + .use { cursor -> + val result = ArraySet(cursor.count) + if (cursor.moveToFirst()) { + do { + result += MangaTag( + key = cursor.getString(COLUMN_KEY), + title = cursor.getString(COLUMN_TITLE), + source = source, + ) + } while (cursor.moveToNext()) + } + result + } + } + + fun getCapabilities(): MangaSourceCapabilities? { + val uri = "content://${source.authority}/capabilities".toUri() + return contentResolver.query(uri, null, null, null, null) + .indexed() + .use { cursor -> + if (cursor.moveToFirst()) { + MangaSourceCapabilities( + availableSortOrders = cursor.getStringOrNull(COLUMN_SORT_ORDERS) + ?.split(',') + ?.mapNotNullTo(EnumSet.noneOf(SortOrder::class.java)) { + SortOrder.entries.find(it) + }.orEmpty(), + availableStates = cursor.getStringOrNull(COLUMN_STATES) + ?.split(',') + ?.mapNotNullTo(EnumSet.noneOf(MangaState::class.java)) { + MangaState.entries.find(it) + }.orEmpty(), + availableContentRating = cursor.getStringOrNull(COLUMN_CONTENT_RATING) + ?.split(',') + ?.mapNotNullTo(EnumSet.noneOf(ContentRating::class.java)) { + ContentRating.entries.find(it) + }.orEmpty(), + isMultipleTagsSupported = cursor.getBooleanOrDefault(COLUMN_MULTIPLE_TAGS_SUPPORTED, true), + isTagsExclusionSupported = cursor.getBooleanOrDefault(COLUMN_TAGS_EXCLUSION_SUPPORTED, false), + isSearchSupported = cursor.getBooleanOrDefault(COLUMN_SEARCH_SUPPORTED, true), + contentType = cursor.getStringOrNull(COLUMN_CONTENT_TYPE)?.let { + ContentType.entries.find(it) + } ?: ContentType.OTHER, + defaultSortOrder = cursor.getStringOrNull(COLUMN_DEFAULT_SORT_ORDER)?.let { + SortOrder.entries.find(it) + } ?: SortOrder.ALPHABETICAL, + sourceLocale = cursor.getStringOrNull(COLUMN_LOCALE)?.toLocale() ?: Locale.ROOT, + ) + } else { + null + } + } + } + + private fun queryDetails(url: String): Manga { + val uri = "content://${source.authority}/manga".toUri() + .buildUpon() + .appendPath(url) + .build() + return contentResolver.query(uri, null, null, null, null) + .indexed() + .use { cursor -> + cursor.moveToFirst() + cursor.getManga() + } + } + + private fun queryChapters(url: String): List { + val uri = "content://${source.authority}/manga/chapters".toUri() + .buildUpon() + .appendPath(url) + .build() + return contentResolver.query(uri, null, null, null, null) + .indexed() + .use { cursor -> + val result = ArrayList(cursor.count) + if (cursor.moveToFirst()) { + do { + result += MangaChapter( + id = cursor.getLong(COLUMN_ID), + name = cursor.getString(COLUMN_NAME), + number = cursor.getFloatOrDefault(COLUMN_NUMBER, 0f), + volume = cursor.getIntOrDefault(COLUMN_VOLUME, 0), + url = cursor.getString(COLUMN_URL), + scanlator = cursor.getStringOrNull(COLUMN_SCANLATOR), + uploadDate = cursor.getLongOrDefault(COLUMN_UPLOAD_DATE, 0L), + branch = cursor.getStringOrNull(COLUMN_BRANCH), + source = source, + ) + } while (cursor.moveToNext()) + } + result + } + } + + private fun IndexedCursor.getManga() = Manga( + id = getLong(COLUMN_ID), + title = getString(COLUMN_TITLE), + altTitle = getStringOrNull(COLUMN_ALT_TITLE), + url = getString(COLUMN_URL), + publicUrl = getString(COLUMN_PUBLIC_URL), + rating = getFloat(COLUMN_RATING), + isNsfw = getBooleanOrDefault(COLUMN_IS_NSFW, false), + coverUrl = getString(COLUMN_COVER_URL), + tags = getStringOrNull(COLUMN_TAGS)?.split(':')?.mapNotNullToSet { + val parts = it.splitTwoParts('=') ?: return@mapNotNullToSet null + MangaTag(key = parts.first, title = parts.second, source = source) + }.orEmpty(), + state = getStringOrNull(COLUMN_STATE)?.let { MangaState.entries.find(it) }, + author = getStringOrNull(COLUMN_AUTHOR), + largeCoverUrl = getStringOrNull(COLUMN_LARGE_COVER_URL), + description = getStringOrNull(COLUMN_DESCRIPTION), + chapters = emptyList(), + source = source, + ) + + private inline fun runCatchingCompatibility(block: () -> R): R = try { + block() + } catch (e: NoSuchElementException) { // unknown column name + throw IncompatiblePluginException(source.name, e) + } catch (e: IllegalArgumentException) { + throw IncompatiblePluginException(source.name, e) + } + + private fun Cursor?.indexed() = IndexedCursor(this ?: throw IncompatiblePluginException(source.name, null)) + + class MangaSourceCapabilities( + val availableSortOrders: Set, + val availableStates: Set, + val availableContentRating: Set, + val isMultipleTagsSupported: Boolean, + val isTagsExclusionSupported: Boolean, + val isSearchSupported: Boolean, + val contentType: ContentType, + val defaultSortOrder: SortOrder, + val sourceLocale: Locale, + ) + + private companion object { + + const val COLUMN_SORT_ORDERS = "sort_orders" + const val COLUMN_STATES = "states" + const val COLUMN_CONTENT_RATING = "content_rating" + const val COLUMN_MULTIPLE_TAGS_SUPPORTED = "multiple_tags_supported" + const val COLUMN_TAGS_EXCLUSION_SUPPORTED = "tags_exclusion_supported" + const val COLUMN_SEARCH_SUPPORTED = "search_supported" + const val COLUMN_CONTENT_TYPE = "content_type" + const val COLUMN_DEFAULT_SORT_ORDER = "default_sort_order" + const val COLUMN_LOCALE = "locale" + const val COLUMN_ID = "id" + const val COLUMN_NAME = "name" + const val COLUMN_NUMBER = "number" + const val COLUMN_VOLUME = "volume" + const val COLUMN_URL = "url" + const val COLUMN_SCANLATOR = "scanlator" + const val COLUMN_UPLOAD_DATE = "upload_date" + const val COLUMN_BRANCH = "branch" + const val COLUMN_TITLE = "title" + const val COLUMN_ALT_TITLE = "alt_title" + const val COLUMN_PUBLIC_URL = "public_url" + const val COLUMN_RATING = "rating" + const val COLUMN_IS_NSFW = "is_nsfw" + const val COLUMN_COVER_URL = "cover_url" + const val COLUMN_TAGS = "tags" + const val COLUMN_STATE = "state" + const val COLUMN_AUTHOR = "author" + const val COLUMN_LARGE_COVER_URL = "large_cover_url" + const val COLUMN_DESCRIPTION = "description" + const val COLUMN_PREVIEW = "preview" + const val COLUMN_KEY = "key" + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/IndexedCursor.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/IndexedCursor.kt new file mode 100644 index 000000000..0ebe258dd --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/IndexedCursor.kt @@ -0,0 +1,80 @@ +package org.koitharu.kotatsu.core.parser.external + +import android.database.Cursor +import android.database.CursorWrapper +import androidx.collection.MutableObjectIntMap +import androidx.collection.ObjectIntMap +import org.koitharu.kotatsu.core.util.ext.getBoolean + +class IndexedCursor(cursor: Cursor) : CursorWrapper(cursor) { + + private val columns: ObjectIntMap = MutableObjectIntMap(columnCount).also { result -> + val names = columnNames + names.forEachIndexed { index, s -> result.put(s, index) } + } + + fun getString(columnName: String): String { + return getString(columns[columnName]) + } + + fun getStringOrNull(columnName: String): String? { + val columnIndex = columns.getOrDefault(columnName, -1) + return when { + columnIndex == -1 -> null + isNull(columnIndex) -> null + else -> getString(columnIndex) + } + } + + fun getBoolean(columnName: String): Boolean { + return getBoolean(columns[columnName]) + } + + fun getBooleanOrDefault(columnName: String, defaultValue: Boolean): Boolean { + val columnIndex = columns.getOrDefault(columnName, -1) + return when { + columnIndex == -1 -> defaultValue + isNull(columnIndex) -> defaultValue + else -> getBoolean(columnIndex) + } + } + + fun getInt(columnName: String): Int { + return getInt(columns[columnName]) + } + + fun getIntOrDefault(columnName: String, defaultValue: Int): Int { + val columnIndex = columns.getOrDefault(columnName, -1) + return when { + columnIndex == -1 -> defaultValue + isNull(columnIndex) -> defaultValue + else -> getInt(columnIndex) + } + } + + fun getLong(columnName: String): Long { + return getLong(columns[columnName]) + } + + fun getLongOrDefault(columnName: String, defaultValue: Long): Long { + val columnIndex = columns.getOrDefault(columnName, -1) + return when { + columnIndex == -1 -> defaultValue + isNull(columnIndex) -> defaultValue + else -> getLong(columnIndex) + } + } + + fun getFloat(columnName: String): Float { + return getFloat(columns[columnName]) + } + + fun getFloatOrDefault(columnName: String, defaultValue: Float): Float { + val columnIndex = columns.getOrDefault(columnName, -1) + return when { + columnIndex == -1 -> defaultValue + isNull(columnIndex) -> defaultValue + else -> getFloat(columnIndex) + } + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Cursor.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Cursor.kt index 3cec3da3b..c7c665d93 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Cursor.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Cursor.kt @@ -37,3 +37,5 @@ fun JSONObject.toContentValues(): ContentValues { } private fun String.escapeName() = "`$this`" + +fun Cursor.getBoolean(columnIndex: Int) = getInt(columnIndex) > 0 diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Throwable.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Throwable.kt index 485d2a6ac..dd38ab04d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Throwable.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Throwable.kt @@ -15,6 +15,7 @@ import org.koitharu.kotatsu.core.exceptions.CaughtException import org.koitharu.kotatsu.core.exceptions.CloudFlareBlockedException import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException +import org.koitharu.kotatsu.core.exceptions.IncompatiblePluginException import org.koitharu.kotatsu.core.exceptions.NoDataReceivedException import org.koitharu.kotatsu.core.exceptions.SyncApiException import org.koitharu.kotatsu.core.exceptions.TooManyRequestExceptions @@ -60,7 +61,7 @@ fun Throwable.getDisplayMessage(resources: Resources): String = when (this) { -> resources.getString(R.string.network_error) is NoDataReceivedException -> resources.getString(R.string.error_no_data_received) - + is IncompatiblePluginException -> resources.getString(R.string.plugin_incompatible) is WrongPasswordException -> resources.getString(R.string.wrong_password) is NotFoundException -> resources.getString(R.string.not_found_404) is UnsupportedSourceException -> resources.getString(R.string.unsupported_source) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4424582d6..227506ecd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -669,4 +669,5 @@ Chapters read Chapters left External/plugin + Incompatible plugin or internal error. Make sure you are using the latest version of the plugin and Kotatsu From 9768758eccd7ceadc1c087d50a9171acbaf2cb52 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Fri, 2 Aug 2024 12:27:42 +0300 Subject: [PATCH 02/24] Optimize external plugin cursor --- app/build.gradle | 6 +-- .../external/ExternalPluginContentSource.kt | 16 ++++---- .../{IndexedCursor.kt => SafeCursor.kt} | 39 ++++++++----------- .../kotatsu/list/domain/ReadingProgress.kt | 2 + 4 files changed, 29 insertions(+), 34 deletions(-) rename app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/{IndexedCursor.kt => SafeCursor.kt} (55%) diff --git a/app/build.gradle b/app/build.gradle index b54b11785..9ecb71250 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,8 +16,8 @@ android { applicationId 'org.koitharu.kotatsu' minSdk = 21 targetSdk = 35 - versionCode = 657 - versionName = '7.4' + versionCode = 658 + versionName = '7.4.1' generatedDensities = [] testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner' ksp { @@ -82,7 +82,7 @@ afterEvaluate { } dependencies { //noinspection GradleDependency - implementation('com.github.KotatsuApp:kotatsu-parsers:a9fc534ea7') { + implementation('com.github.KotatsuApp:kotatsu-parsers:853c21e49f') { exclude group: 'org.json', module: 'json' } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/ExternalPluginContentSource.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/ExternalPluginContentSource.kt index 5e7d9a800..960bbdd2c 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/ExternalPluginContentSource.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/ExternalPluginContentSource.kt @@ -50,7 +50,7 @@ class ExternalPluginContentSource( null -> Unit } contentResolver.query(uri.build(), null, null, null, filter?.sortOrder?.name) - .indexed() + .safe() .use { cursor -> val result = ArrayList(cursor.count) if (cursor.moveToFirst()) { @@ -94,7 +94,7 @@ class ExternalPluginContentSource( .appendPath(chapter.url) .build() contentResolver.query(uri, null, null, null, null) - .indexed() + .safe() .use { cursor -> val result = ArrayList(cursor.count) if (cursor.moveToFirst()) { @@ -116,7 +116,7 @@ class ExternalPluginContentSource( fun getTags(): Set = runCatchingCompatibility { val uri = "content://${source.authority}/tags".toUri() contentResolver.query(uri, null, null, null, null) - .indexed() + .safe() .use { cursor -> val result = ArraySet(cursor.count) if (cursor.moveToFirst()) { @@ -135,7 +135,7 @@ class ExternalPluginContentSource( fun getCapabilities(): MangaSourceCapabilities? { val uri = "content://${source.authority}/capabilities".toUri() return contentResolver.query(uri, null, null, null, null) - .indexed() + .safe() .use { cursor -> if (cursor.moveToFirst()) { MangaSourceCapabilities( @@ -177,7 +177,7 @@ class ExternalPluginContentSource( .appendPath(url) .build() return contentResolver.query(uri, null, null, null, null) - .indexed() + .safe() .use { cursor -> cursor.moveToFirst() cursor.getManga() @@ -190,7 +190,7 @@ class ExternalPluginContentSource( .appendPath(url) .build() return contentResolver.query(uri, null, null, null, null) - .indexed() + .safe() .use { cursor -> val result = ArrayList(cursor.count) if (cursor.moveToFirst()) { @@ -212,7 +212,7 @@ class ExternalPluginContentSource( } } - private fun IndexedCursor.getManga() = Manga( + private fun SafeCursor.getManga() = Manga( id = getLong(COLUMN_ID), title = getString(COLUMN_TITLE), altTitle = getStringOrNull(COLUMN_ALT_TITLE), @@ -241,7 +241,7 @@ class ExternalPluginContentSource( throw IncompatiblePluginException(source.name, e) } - private fun Cursor?.indexed() = IndexedCursor(this ?: throw IncompatiblePluginException(source.name, null)) + private fun Cursor?.safe() = SafeCursor(this ?: throw IncompatiblePluginException(source.name, null)) class MangaSourceCapabilities( val availableSortOrders: Set, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/IndexedCursor.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/SafeCursor.kt similarity index 55% rename from app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/IndexedCursor.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/SafeCursor.kt index 0ebe258dd..e7ac48455 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/IndexedCursor.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/SafeCursor.kt @@ -2,77 +2,70 @@ package org.koitharu.kotatsu.core.parser.external import android.database.Cursor import android.database.CursorWrapper -import androidx.collection.MutableObjectIntMap -import androidx.collection.ObjectIntMap import org.koitharu.kotatsu.core.util.ext.getBoolean -class IndexedCursor(cursor: Cursor) : CursorWrapper(cursor) { - - private val columns: ObjectIntMap = MutableObjectIntMap(columnCount).also { result -> - val names = columnNames - names.forEachIndexed { index, s -> result.put(s, index) } - } +class SafeCursor(cursor: Cursor) : CursorWrapper(cursor) { fun getString(columnName: String): String { - return getString(columns[columnName]) + return getString(getColumnIndexOrThrow(columnName)) } fun getStringOrNull(columnName: String): String? { - val columnIndex = columns.getOrDefault(columnName, -1) + val columnIndex = getColumnIndex(columnName) return when { - columnIndex == -1 -> null + columnIndex < 0 -> null isNull(columnIndex) -> null else -> getString(columnIndex) } } fun getBoolean(columnName: String): Boolean { - return getBoolean(columns[columnName]) + return getBoolean(getColumnIndexOrThrow(columnName)) } fun getBooleanOrDefault(columnName: String, defaultValue: Boolean): Boolean { - val columnIndex = columns.getOrDefault(columnName, -1) + val columnIndex = getColumnIndex(columnName) return when { - columnIndex == -1 -> defaultValue + columnIndex < 0 -> defaultValue isNull(columnIndex) -> defaultValue else -> getBoolean(columnIndex) } } fun getInt(columnName: String): Int { - return getInt(columns[columnName]) + return getInt(getColumnIndexOrThrow(columnName)) } fun getIntOrDefault(columnName: String, defaultValue: Int): Int { - val columnIndex = columns.getOrDefault(columnName, -1) + val columnIndex = getColumnIndex(columnName) return when { - columnIndex == -1 -> defaultValue + columnIndex < 0 -> defaultValue isNull(columnIndex) -> defaultValue else -> getInt(columnIndex) } } fun getLong(columnName: String): Long { - return getLong(columns[columnName]) + return getLong(getColumnIndexOrThrow(columnName)) } fun getLongOrDefault(columnName: String, defaultValue: Long): Long { - val columnIndex = columns.getOrDefault(columnName, -1) + val columnIndex = getColumnIndex(columnName) return when { - columnIndex == -1 -> defaultValue + columnIndex < 0 -> defaultValue isNull(columnIndex) -> defaultValue else -> getLong(columnIndex) } } fun getFloat(columnName: String): Float { - return getFloat(columns[columnName]) + return getFloat(getColumnIndexOrThrow(columnName)) } fun getFloatOrDefault(columnName: String, defaultValue: Float): Float { - val columnIndex = columns.getOrDefault(columnName, -1) + val columnIndex = getColumnIndex(columnName) return when { - columnIndex == -1 -> defaultValue + columnIndex < 0 -> defaultValue isNull(columnIndex) -> defaultValue else -> getFloat(columnIndex) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/ReadingProgress.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/ReadingProgress.kt index e0120adcb..4b6820225 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/ReadingProgress.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/ReadingProgress.kt @@ -31,5 +31,7 @@ data class ReadingProgress( CHAPTERS_LEFT -> totalChapters > 0 && percent in 0f..1f } + fun isComplete() = percent >= 1f - Math.ulp(percent) + fun isReversed() = mode == PERCENT_LEFT || mode == CHAPTERS_LEFT } From 250d5432a0be9761e6cb9577477dbc52b4a67a43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B0=D0=BA=D0=B0=D1=80=20=D0=A0=D0=B0=D0=B7=D0=B8?= =?UTF-8?q?=D0=BD?= Date: Sat, 3 Aug 2024 11:47:41 +0200 Subject: [PATCH 03/24] Translated using Weblate (Russian) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (663 of 663 strings) Translated using Weblate (Belarusian) Currently translated at 100.0% (663 of 663 strings) Co-authored-by: Макар Разин Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/ Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/ Translation: Kotatsu/Strings --- app/src/main/res/values-be/strings.xml | 5 +++++ app/src/main/res/values-ru/strings.xml | 1 + 2 files changed, 6 insertions(+) diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index 30863bd95..9ab2b6aff 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -653,4 +653,9 @@ Нядаўнія крыніцы Крыніцы замацаваны Абрэзаць старонкі + Працэнт прачытанага + Астатні працэнт + Прачытаныя раздзелы + Астатнія раздзелы + Знешні/плагін \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index f2fb51395..8f7d6d3ec 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -657,4 +657,5 @@ Глав прочитано Процент оставшегося Глав осталось + Внешний/плагин \ No newline at end of file From 207ea492d587aac9ef0d42cb210763099e3f3e84 Mon Sep 17 00:00:00 2001 From: TheOneWhoCares <266nre4gw@mozmail.com> Date: Sat, 3 Aug 2024 11:47:41 +0200 Subject: [PATCH 04/24] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (663 of 663 strings) Co-authored-by: TheOneWhoCares <266nre4gw@mozmail.com> Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/ Translation: Kotatsu/Strings --- app/src/main/res/values-pt-rBR/strings.xml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 6455f03ef..03fcefc73 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -644,4 +644,18 @@ Cortar páginas Desativar notificações NSFW Não mostrar notificações sobre atualizações de mangás NSFW + Porcentagem lido + Porcentagem restante + Capítulos lidos + Capítulos restantes + Pin + Unpin + Fonte destacada + Fonte não destacada + Fontes destacadas + Fontes recentes + Plugin/Externo + Fontes não destacadas + Checando por novos logs de capítulos + Informações de Debug sobre a checagem de fundo para novos capítulos \ No newline at end of file From 2f33a135fc1f710d850cf6182deaf548a8702fac Mon Sep 17 00:00:00 2001 From: weedyy Date: Sat, 3 Aug 2024 11:47:41 +0200 Subject: [PATCH 05/24] Translated using Weblate (Arabic) Currently translated at 99.8% (662 of 663 strings) Co-authored-by: weedyy Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ar/ Translation: Kotatsu/Strings --- app/src/main/res/values-ar/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index da3948b8c..e68b24eaa 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -383,7 +383,7 @@ يمكنك تسجيل الدخول إلى حساب موجود أصلا أو إنشاء حساب جديد متوقف مؤقتاً التحميل عبر شبكة الوايفاي فقط - إظهار الإشعارات أحيانًا بالمانغا المقترحة + إظهار الإشعارات أحيانًا بالمانجا المقترحة اللغة العربية ‌‌‍‎‎‍هل ترغب في تلقي اقتراحات المانجا الشخصية؟ وكيل تحسين الصور @@ -515,7 +515,7 @@ س%.1f تخطى تدرج الرمادي - عالماً + عالمياً هذه المانجا يمكن تطبيق هذه الإعدادات عالمياً أو على المانجا الحالية فقط. إذا تم تطبيقه عالمياً، فلن يتم تجاوز الإعدادات الفردية. طَبِق @@ -602,7 +602,7 @@ عرض التحديثات موحية تهيئة الإجراءات لمناطق الشاشة القابلة للنقر عليها - اطلب دليل الوجهة في كل مرة + اطلب وجهة المجلد في كل مرة مجلد حفظ الصفحة الافتراضية تنسيق التحميل المُفضل تلقائي From 966d6e2383e2446a450c5c066224174c2d2aeff1 Mon Sep 17 00:00:00 2001 From: Draken Date: Sat, 3 Aug 2024 11:47:41 +0200 Subject: [PATCH 06/24] Translated using Weblate (Vietnamese) Currently translated at 100.0% (663 of 663 strings) Co-authored-by: Draken Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/ Translation: Kotatsu/Strings --- app/src/main/res/values-vi/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index f39046063..fbbfc6893 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -657,4 +657,5 @@ Tiến trình đọc còn lại Chương đã đọc Chương còn lại + Nguồn / Plugin bên ngoài \ No newline at end of file From 1618a1195534584c0ca2496008ff4cf798ab8354 Mon Sep 17 00:00:00 2001 From: Anon Date: Sat, 3 Aug 2024 11:47:41 +0200 Subject: [PATCH 07/24] Translated using Weblate (Serbian) Currently translated at 100.0% (663 of 663 strings) Co-authored-by: Anon Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/ Translation: Kotatsu/Strings --- app/src/main/res/values-sr/strings.xml | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index 6d61b797d..db821ea77 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -2,7 +2,7 @@ Локално складиште Грешка се појавила - Омиљено + Омиљене Историја Грешка на мрежи Детаљи @@ -117,7 +117,7 @@ Прикажи на полици Истражи Опције - Додај у омиљене + Додај у Омиљене Пронађи ствари за читање у одељку „Истражи“ Показатељ ЛЕД светла Омиљене категорије @@ -151,7 +151,7 @@ Откажи све Можеш да користиш послуживач за синхронизацију који се самостално хостује или подразумевани. Не мењај ово ако ниси сигуран шта радиш. Враћени су неважећи подаци или је датотека оштећена - Сви омиљени + Све омиљене Унесите своју адресу е-поште да бисте наставили Изабери прилагођени директоријум Нема поглавља @@ -189,7 +189,7 @@ Избриши сву историју Брисање података Прикажи недавне пречице за мангу - Заустави преузимање када пређеш на мобилну мрежу + Зауставља преузимање када пређеш на мобилну мрежу Предложи нове изворе након ажурирања апликације Увоз је завршен Прикажи показивач током читања @@ -252,7 +252,7 @@ Подаци нису враћени Управљај изворима Директоријуми - Локални директорији манги + Локални директоријуми Манги Управљај категоријама Ажурирај Да бисте пратили напредак читања, изаберите Изборник → Прати на екрану са детаљима манге. @@ -308,7 +308,7 @@ DNS преко HTTPS-а Прикажи сумњив садржај Синхронизујте своје податке - Манга из ваших омиљених + Манга из ваших Омиљених Можеш да изабереш једну или више .cbz или .zip датотека, свака датотека ће бити препозната као засебна манга. Преузимања су заустављена Превише захтева. Покушај поново касније @@ -400,7 +400,7 @@ Означи као тренутно Затражи лозинку при покретању Котатсу-а Са десна на лево - Прикажи проценат читања у историји и омиљеним + Прикажи проценат читања у Историји и Омиљеним Насумично Аутоматски изабери послуживач Користи отисак прста ако је доступан @@ -495,7 +495,7 @@ Остало Предлог: %s Црна - Уклоњено из омиљених + Уклоњено из Омиљених Обележивачи Покажи све Овог месеца @@ -657,4 +657,5 @@ Недавни извори Жељени послуживач слика Изрежи странице + Спољни/додатак \ No newline at end of file From 125b6740a6e980ce0a8e799ee5c98cd2c5d0a01e Mon Sep 17 00:00:00 2001 From: Scrambled777 Date: Sat, 3 Aug 2024 11:47:41 +0200 Subject: [PATCH 08/24] Translated using Weblate (Hindi) Currently translated at 100.0% (664 of 664 strings) Translated using Weblate (Hindi) Currently translated at 100.0% (663 of 663 strings) Co-authored-by: Scrambled777 Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hi/ Translation: Kotatsu/Strings --- app/src/main/res/values-hi/strings.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index f476606de..481034a04 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -653,4 +653,10 @@ स्रोत पिन किया गया स्रोत अनपिन किया गया हालिया स्रोत + प्रतिशत पढ़ा + प्रतिशत शेष + अध्याय पढ़ा + बाहरी/प्लगइन + अध्याय शेष + असंगत प्लगइन या आंतरिक त्रुटि। सुनिश्चित करें कि आप प्लगइन और कोटात्सु के नवीनतम संस्करण का उपयोग कर रहे हैं \ No newline at end of file From f477797823f4104f0935313484f0e954a210b9f8 Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Sat, 3 Aug 2024 11:47:41 +0200 Subject: [PATCH 09/24] Translated using Weblate (Spanish) Currently translated at 100.0% (664 of 664 strings) Co-authored-by: gallegonovato Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/ Translation: Kotatsu/Strings --- app/src/main/res/values-es/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 359bdc190..a5124fc1d 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -658,4 +658,5 @@ Capítulos leídos Capítulos restantes Externo/plugin + Complemento incompatible o error interno. Asegúrate de estar usando la última versión del complemento y de Kotatsu \ No newline at end of file From 7e6e1fb6ded71f93d332bb94db090490c9e847ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?O=C4=9Fuz=20Ersen?= Date: Sat, 3 Aug 2024 11:47:41 +0200 Subject: [PATCH 10/24] Translated using Weblate (Turkish) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (664 of 664 strings) Co-authored-by: Oğuz Ersen Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/ Translation: Kotatsu/Strings --- app/src/main/res/values-tr/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index a2bdd4818..237cf11c1 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -658,4 +658,5 @@ Okunan bölüm Kalan bölüm Harici/eklenti + Uyumsuz eklenti veya dahili hata. Eklentinin ve Kotatsu\'nun en son sürümünü kullandığınızdan emin olun \ No newline at end of file From 4a03137a25fe252c8b13a3f06915aec8b4968b6a Mon Sep 17 00:00:00 2001 From: gekka <1778962971@qq.com> Date: Sat, 3 Aug 2024 11:47:41 +0200 Subject: [PATCH 11/24] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (664 of 664 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (664 of 664 strings) Co-authored-by: gekka <1778962971@qq.com> Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/ Translation: Kotatsu/Strings --- app/src/main/res/values-zh-rCN/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index bf1f5ea13..65c310861 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -617,7 +617,7 @@ %1$d 时 %2$d 分 修复 无访问外部存储漫画权限 - 最近使用 + 上次使用 显示更新 在条漫模式下添加页与页之间的横向缝隙 缝隙条漫模式 @@ -658,4 +658,5 @@ 已读章节数 剩余章节数 外部插件 + 插件不兼容或出现了外部错误,请确保你已经将 Kotatsu 以及插件更新至最新版本 \ No newline at end of file From e92f1656775f075ad9dcd375188e4ec5d9f96a51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B0=D0=BA=D0=B0=D1=80=20=D0=A0=D0=B0=D0=B7=D0=B8?= =?UTF-8?q?=D0=BD?= Date: Fri, 2 Aug 2024 21:09:29 +0200 Subject: [PATCH 12/24] Translated using Weblate (Russian) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (663 of 663 strings) Translated using Weblate (Belarusian) Currently translated at 100.0% (663 of 663 strings) Co-authored-by: Макар Разин Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/ Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/ Translation: Kotatsu/Strings --- app/src/main/res/values-be/strings.xml | 5 +++++ app/src/main/res/values-ru/strings.xml | 1 + 2 files changed, 6 insertions(+) diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index 30863bd95..9ab2b6aff 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -653,4 +653,9 @@ Нядаўнія крыніцы Крыніцы замацаваны Абрэзаць старонкі + Працэнт прачытанага + Астатні працэнт + Прачытаныя раздзелы + Астатнія раздзелы + Знешні/плагін \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index f2fb51395..8f7d6d3ec 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -657,4 +657,5 @@ Глав прочитано Процент оставшегося Глав осталось + Внешний/плагин \ No newline at end of file From 94670a03ff7f637f8ba0a21987aaffd0878dddf5 Mon Sep 17 00:00:00 2001 From: TheOneWhoCares <266nre4gw@mozmail.com> Date: Fri, 2 Aug 2024 21:09:31 +0200 Subject: [PATCH 13/24] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (663 of 663 strings) Co-authored-by: TheOneWhoCares <266nre4gw@mozmail.com> Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/ Translation: Kotatsu/Strings --- app/src/main/res/values-pt-rBR/strings.xml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 6455f03ef..03fcefc73 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -644,4 +644,18 @@ Cortar páginas Desativar notificações NSFW Não mostrar notificações sobre atualizações de mangás NSFW + Porcentagem lido + Porcentagem restante + Capítulos lidos + Capítulos restantes + Pin + Unpin + Fonte destacada + Fonte não destacada + Fontes destacadas + Fontes recentes + Plugin/Externo + Fontes não destacadas + Checando por novos logs de capítulos + Informações de Debug sobre a checagem de fundo para novos capítulos \ No newline at end of file From 1ecf41611368950c48dc465a0d77777b7ae7d1f7 Mon Sep 17 00:00:00 2001 From: weedyy Date: Fri, 2 Aug 2024 21:09:32 +0200 Subject: [PATCH 14/24] Translated using Weblate (Arabic) Currently translated at 99.8% (662 of 663 strings) Co-authored-by: weedyy Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ar/ Translation: Kotatsu/Strings --- app/src/main/res/values-ar/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index da3948b8c..e68b24eaa 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -383,7 +383,7 @@ يمكنك تسجيل الدخول إلى حساب موجود أصلا أو إنشاء حساب جديد متوقف مؤقتاً التحميل عبر شبكة الوايفاي فقط - إظهار الإشعارات أحيانًا بالمانغا المقترحة + إظهار الإشعارات أحيانًا بالمانجا المقترحة اللغة العربية ‌‌‍‎‎‍هل ترغب في تلقي اقتراحات المانجا الشخصية؟ وكيل تحسين الصور @@ -515,7 +515,7 @@ س%.1f تخطى تدرج الرمادي - عالماً + عالمياً هذه المانجا يمكن تطبيق هذه الإعدادات عالمياً أو على المانجا الحالية فقط. إذا تم تطبيقه عالمياً، فلن يتم تجاوز الإعدادات الفردية. طَبِق @@ -602,7 +602,7 @@ عرض التحديثات موحية تهيئة الإجراءات لمناطق الشاشة القابلة للنقر عليها - اطلب دليل الوجهة في كل مرة + اطلب وجهة المجلد في كل مرة مجلد حفظ الصفحة الافتراضية تنسيق التحميل المُفضل تلقائي From 4f454ab4385a817ce0773ff56e51156b02c15de7 Mon Sep 17 00:00:00 2001 From: Draken Date: Fri, 2 Aug 2024 21:09:34 +0200 Subject: [PATCH 15/24] Translated using Weblate (Vietnamese) Currently translated at 100.0% (663 of 663 strings) Co-authored-by: Draken Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/ Translation: Kotatsu/Strings --- app/src/main/res/values-vi/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index f39046063..fbbfc6893 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -657,4 +657,5 @@ Tiến trình đọc còn lại Chương đã đọc Chương còn lại + Nguồn / Plugin bên ngoài \ No newline at end of file From b207eebe56a2e3de80ddef0878b72a01442f555d Mon Sep 17 00:00:00 2001 From: Anon Date: Fri, 2 Aug 2024 21:09:36 +0200 Subject: [PATCH 16/24] Translated using Weblate (Serbian) Currently translated at 100.0% (663 of 663 strings) Co-authored-by: Anon Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/ Translation: Kotatsu/Strings --- app/src/main/res/values-sr/strings.xml | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index 6d61b797d..db821ea77 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -2,7 +2,7 @@ Локално складиште Грешка се појавила - Омиљено + Омиљене Историја Грешка на мрежи Детаљи @@ -117,7 +117,7 @@ Прикажи на полици Истражи Опције - Додај у омиљене + Додај у Омиљене Пронађи ствари за читање у одељку „Истражи“ Показатељ ЛЕД светла Омиљене категорије @@ -151,7 +151,7 @@ Откажи све Можеш да користиш послуживач за синхронизацију који се самостално хостује или подразумевани. Не мењај ово ако ниси сигуран шта радиш. Враћени су неважећи подаци или је датотека оштећена - Сви омиљени + Све омиљене Унесите своју адресу е-поште да бисте наставили Изабери прилагођени директоријум Нема поглавља @@ -189,7 +189,7 @@ Избриши сву историју Брисање података Прикажи недавне пречице за мангу - Заустави преузимање када пређеш на мобилну мрежу + Зауставља преузимање када пређеш на мобилну мрежу Предложи нове изворе након ажурирања апликације Увоз је завршен Прикажи показивач током читања @@ -252,7 +252,7 @@ Подаци нису враћени Управљај изворима Директоријуми - Локални директорији манги + Локални директоријуми Манги Управљај категоријама Ажурирај Да бисте пратили напредак читања, изаберите Изборник → Прати на екрану са детаљима манге. @@ -308,7 +308,7 @@ DNS преко HTTPS-а Прикажи сумњив садржај Синхронизујте своје податке - Манга из ваших омиљених + Манга из ваших Омиљених Можеш да изабереш једну или више .cbz или .zip датотека, свака датотека ће бити препозната као засебна манга. Преузимања су заустављена Превише захтева. Покушај поново касније @@ -400,7 +400,7 @@ Означи као тренутно Затражи лозинку при покретању Котатсу-а Са десна на лево - Прикажи проценат читања у историји и омиљеним + Прикажи проценат читања у Историји и Омиљеним Насумично Аутоматски изабери послуживач Користи отисак прста ако је доступан @@ -495,7 +495,7 @@ Остало Предлог: %s Црна - Уклоњено из омиљених + Уклоњено из Омиљених Обележивачи Покажи све Овог месеца @@ -657,4 +657,5 @@ Недавни извори Жељени послуживач слика Изрежи странице + Спољни/додатак \ No newline at end of file From f4838afab08c619a04ef99fc37f3ee465881a68f Mon Sep 17 00:00:00 2001 From: Scrambled777 Date: Fri, 2 Aug 2024 21:09:38 +0200 Subject: [PATCH 17/24] Translated using Weblate (Hindi) Currently translated at 100.0% (664 of 664 strings) Translated using Weblate (Hindi) Currently translated at 100.0% (663 of 663 strings) Co-authored-by: Scrambled777 Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hi/ Translation: Kotatsu/Strings --- app/src/main/res/values-hi/strings.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index f476606de..481034a04 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -653,4 +653,10 @@ स्रोत पिन किया गया स्रोत अनपिन किया गया हालिया स्रोत + प्रतिशत पढ़ा + प्रतिशत शेष + अध्याय पढ़ा + बाहरी/प्लगइन + अध्याय शेष + असंगत प्लगइन या आंतरिक त्रुटि। सुनिश्चित करें कि आप प्लगइन और कोटात्सु के नवीनतम संस्करण का उपयोग कर रहे हैं \ No newline at end of file From 34dd080f6c9fc242b2118282040863528002731a Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Fri, 2 Aug 2024 21:09:40 +0200 Subject: [PATCH 18/24] Translated using Weblate (Spanish) Currently translated at 100.0% (664 of 664 strings) Co-authored-by: gallegonovato Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/ Translation: Kotatsu/Strings --- app/src/main/res/values-es/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 359bdc190..a5124fc1d 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -658,4 +658,5 @@ Capítulos leídos Capítulos restantes Externo/plugin + Complemento incompatible o error interno. Asegúrate de estar usando la última versión del complemento y de Kotatsu \ No newline at end of file From 1efe86421ae38d38597f8faf90d9652b4b455737 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?O=C4=9Fuz=20Ersen?= Date: Fri, 2 Aug 2024 21:09:42 +0200 Subject: [PATCH 19/24] Translated using Weblate (Turkish) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (664 of 664 strings) Co-authored-by: Oğuz Ersen Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/ Translation: Kotatsu/Strings --- app/src/main/res/values-tr/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index a2bdd4818..237cf11c1 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -658,4 +658,5 @@ Okunan bölüm Kalan bölüm Harici/eklenti + Uyumsuz eklenti veya dahili hata. Eklentinin ve Kotatsu\'nun en son sürümünü kullandığınızdan emin olun \ No newline at end of file From 4044936481b4f9d62f64e9fab66071b630ad58e7 Mon Sep 17 00:00:00 2001 From: gekka <1778962971@qq.com> Date: Fri, 2 Aug 2024 21:09:44 +0200 Subject: [PATCH 20/24] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (664 of 664 strings) Co-authored-by: gekka <1778962971@qq.com> Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/ Translation: Kotatsu/Strings --- app/src/main/res/values-zh-rCN/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index bf1f5ea13..0069d9469 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -617,7 +617,7 @@ %1$d 时 %2$d 分 修复 无访问外部存储漫画权限 - 最近使用 + 上次使用 显示更新 在条漫模式下添加页与页之间的横向缝隙 缝隙条漫模式 @@ -658,4 +658,5 @@ 已读章节数 剩余章节数 外部插件 + 插件不兼容或出现了外部错误,请确保你已经更新到最新版本的 Kotatsu 以及插件 \ No newline at end of file From 1a17324d260f7ffe6f51ce68374b0b46fd4c0ab5 Mon Sep 17 00:00:00 2001 From: vianh Date: Tue, 30 Jul 2024 18:45:13 +0700 Subject: [PATCH 21/24] Fix reader state not being restored --- .../kotatsu/reader/ui/ReaderViewModel.kt | 3 ++- .../reader/ui/pager/BaseReaderFragment.kt | 23 ++++--------------- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt index 38ccf2da1..759287ec9 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt @@ -73,7 +73,7 @@ private const val PREFETCH_LIMIT = 10 class ReaderViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, + private val savedStateHandle: SavedStateHandle, private val dataRepository: MangaDataRepository, private val historyRepository: HistoryRepository, private val bookmarksRepository: BookmarksRepository, @@ -223,6 +223,7 @@ constructor( fun saveCurrentState(state: ReaderState? = null) { if (state != null) { currentState.value = state + savedStateHandle[ReaderActivity.EXTRA_STATE] = state } if (incognitoMode.value) { return diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/BaseReaderFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/BaseReaderFragment.kt index adec238ec..04fa68a81 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/BaseReaderFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/BaseReaderFragment.kt @@ -13,28 +13,23 @@ import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.reader.ui.ReaderState import org.koitharu.kotatsu.reader.ui.ReaderViewModel -private const val KEY_STATE = "state" - abstract class BaseReaderFragment : BaseFragment(), ZoomControl.ZoomControlListener { protected val viewModel by activityViewModels() - private var stateToSave: ReaderState? = null protected var readerAdapter: BaseReaderAdapter<*>? = null private set override fun onViewBindingCreated(binding: B, savedInstanceState: Bundle?) { super.onViewBindingCreated(binding, savedInstanceState) - var restoredState = savedInstanceState?.getParcelableCompat(KEY_STATE) readerAdapter = onCreateAdapter() viewModel.content.observe(viewLifecycleOwner) { - var pendingState = restoredState ?: it.state - if (pendingState == null && it.pages.isNotEmpty() && readerAdapter?.hasItems != true) { - pendingState = viewModel.getCurrentState() + if (it.state == null && it.pages.isNotEmpty() && readerAdapter?.hasItems != true) { + onPagesChanged(it.pages, viewModel.getCurrentState()) + } else { + onPagesChanged(it.pages, it.state) } - onPagesChanged(it.pages, pendingState) - restoredState = null } } @@ -44,19 +39,11 @@ abstract class BaseReaderFragment : BaseFragment(), ZoomCont } override fun onDestroyView() { - stateToSave = getCurrentState() + viewModel.saveCurrentState(getCurrentState()) readerAdapter = null super.onDestroyView() } - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - getCurrentState()?.let { - stateToSave = it - } - outState.putParcelable(KEY_STATE, stateToSave) - } - protected fun requireAdapter() = checkNotNull(readerAdapter) { "Adapter was not created or already destroyed" } From b7741ce2af4d1ae5fe2b2623253ff1844def13ae Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 3 Aug 2024 13:35:28 +0300 Subject: [PATCH 22/24] Allow to use biometric unlock manually (closes #999) --- .../main/ui/protect/ProtectActivity.kt | 27 ++++++++++++++++++- app/src/main/res/values/strings.xml | 2 +- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/protect/ProtectActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/protect/ProtectActivity.kt index 8ad250394..9516a8447 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/protect/ProtectActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/protect/ProtectActivity.kt @@ -17,6 +17,7 @@ import androidx.biometric.BiometricManager.BIOMETRIC_SUCCESS import androidx.biometric.BiometricPrompt import androidx.biometric.BiometricPrompt.AuthenticationCallback import androidx.core.graphics.Insets +import com.google.android.material.textfield.TextInputLayout import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.ui.BaseActivity @@ -25,6 +26,7 @@ import org.koitharu.kotatsu.core.util.ext.getParcelableExtraCompat import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.databinding.ActivityProtectBinding +import com.google.android.material.R as materialR @AndroidEntryPoint class ProtectActivity : @@ -34,6 +36,7 @@ class ProtectActivity : View.OnClickListener { private val viewModel by viewModels() + private var canUseBiometric = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -61,7 +64,9 @@ class ProtectActivity : override fun onStart() { super.onStart() - if (!useFingerprint()) { + canUseBiometric = useFingerprint() + updateEndIcon() + if (!canUseBiometric) { viewBinding.editPassword.requestFocus() } } @@ -80,6 +85,7 @@ class ProtectActivity : when (v.id) { R.id.button_next -> viewModel.tryUnlock(viewBinding.editPassword.text?.toString().orEmpty()) R.id.button_cancel -> finish() + materialR.id.text_input_end_icon -> useFingerprint() } } @@ -99,6 +105,7 @@ class ProtectActivity : override fun afterTextChanged(s: Editable?) { viewBinding.layoutPassword.error = null viewBinding.buttonNext.isEnabled = !s.isNullOrEmpty() + updateEndIcon() } private fun onError(e: Throwable) { @@ -127,6 +134,24 @@ class ProtectActivity : return true } + private fun updateEndIcon() = with(viewBinding.layoutPassword) { + val isFingerprintIcon = canUseBiometric && viewBinding.editPassword.text.isNullOrEmpty() + if (isFingerprintIcon == (endIconMode == TextInputLayout.END_ICON_CUSTOM)) { + return@with + } + if (isFingerprintIcon) { + endIconMode = TextInputLayout.END_ICON_CUSTOM + setEndIconDrawable(androidx.biometric.R.drawable.fingerprint_dialog_fp_icon) + endIconContentDescription = getString(androidx.biometric.R.string.use_biometric_label) + setEndIconOnClickListener(this@ProtectActivity) + } else { + setEndIconOnClickListener(null) + setEndIconDrawable(0) + endIconContentDescription = null + endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE + } + } + private inner class BiometricCallback : AuthenticationCallback() { override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { super.onAuthenticationSucceeded(result) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 227506ecd..a3ed043af 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -267,7 +267,7 @@ On hold Dropped Disable all - Use fingerprint if available + Use biometric if available Manga from your favourites Your recently read manga Report From 6e92d46a6342e53510624688e485619798eb6292 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 3 Aug 2024 15:02:31 +0300 Subject: [PATCH 23/24] Update parsers --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 9ecb71250..f850ab3fb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -82,7 +82,7 @@ afterEvaluate { } dependencies { //noinspection GradleDependency - implementation('com.github.KotatsuApp:kotatsu-parsers:853c21e49f') { + implementation('com.github.KotatsuApp:kotatsu-parsers:3b5a018f8c') { exclude group: 'org.json', module: 'json' } From d00822a6c3d8c39e5a23ccb49dcbc41ec23d2412 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 3 Aug 2024 16:22:46 +0300 Subject: [PATCH 24/24] Quick filter in history draft implementation --- .../kotatsu/core/ui/widgets/ChipsView.kt | 10 ++- .../kotatsu/history/data/HistoryDao.kt | 21 ++++- .../kotatsu/history/data/HistoryRepository.kt | 9 ++- .../kotatsu/history/ui/HistoryListFragment.kt | 5 ++ .../history/ui/HistoryListViewModel.kt | 80 +++++++++++++++---- .../kotatsu/list/domain/ListFilterOption.kt | 28 +++++++ .../kotatsu/list/ui/MangaListFragment.kt | 3 + .../kotatsu/list/ui/adapter/ListItemType.kt | 1 + .../list/ui/adapter/MangaListAdapter.kt | 1 + .../ui/adapter/MangaListDetailedItemAD.kt | 2 +- .../list/ui/adapter/MangaListListener.kt | 2 +- .../kotatsu/list/ui/adapter/QuickFilterAD.kt | 25 ++++++ .../ui/adapter/QuickFilterClickListener.kt | 8 ++ .../ui/adapter/TypedListSpacingDecoration.kt | 1 + .../kotatsu/list/ui/model/QuickFilter.kt | 13 +++ .../search/ui/multi/MultiSearchActivity.kt | 3 + .../kotatsu/tracker/ui/feed/FeedFragment.kt | 3 + app/src/main/res/layout/item_quick_filter.xml | 24 ++++++ app/src/main/res/values/strings.xml | 1 + 19 files changed, 218 insertions(+), 22 deletions(-) create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/list/domain/ListFilterOption.kt create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/QuickFilterAD.kt create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/QuickFilterClickListener.kt create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/QuickFilter.kt create mode 100644 app/src/main/res/layout/item_quick_filter.xml 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 fd332ca5b..66a5e9227 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 @@ -5,6 +5,7 @@ import android.util.AttributeSet import android.view.View.OnClickListener import androidx.annotation.ColorRes import androidx.annotation.DrawableRes +import androidx.annotation.StringRes import androidx.core.view.children import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipDrawable @@ -92,7 +93,11 @@ class ChipsView @JvmOverloads constructor( } private fun bindChip(chip: Chip, model: ChipModel) { - chip.text = model.title + if (model.titleResId == 0) { + chip.text = model.title + } else { + chip.setText(model.titleResId) + } chip.isClickable = onChipClickListener != null || model.isCheckable chip.isCheckable = model.isCheckable if (model.icon == 0) { @@ -139,7 +144,8 @@ class ChipsView @JvmOverloads constructor( } data class ChipModel( - val title: CharSequence, + val title: CharSequence? = null, + @StringRes val titleResId: Int = 0, @DrawableRes val icon: Int = 0, val isCheckable: Boolean = false, @ColorRes val tint: Int = 0, 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 93b3b8db2..b54ddb1e4 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 @@ -10,6 +10,7 @@ import androidx.sqlite.db.SimpleSQLiteQuery import androidx.sqlite.db.SupportSQLiteQuery import kotlinx.coroutines.flow.Flow import org.koitharu.kotatsu.core.db.entity.TagEntity +import org.koitharu.kotatsu.list.domain.ListFilterOption import org.koitharu.kotatsu.list.domain.ListSortOrder @Dao @@ -27,7 +28,11 @@ abstract class HistoryDao { @Query("SELECT * FROM history WHERE deleted_at = 0 ORDER BY updated_at DESC LIMIT :limit") abstract fun observeAll(limit: Int): Flow> - fun observeAll(order: ListSortOrder, limit: Int): Flow> { + fun observeAll( + order: ListSortOrder, + filterOptions: Set, + limit: Int + ): Flow> { val orderBy = when (order) { ListSortOrder.LAST_READ -> "history.updated_at DESC" ListSortOrder.LONG_AGO_READ -> "history.updated_at ASC" @@ -44,8 +49,13 @@ abstract class HistoryDao { val query = buildString { append( "SELECT * FROM history LEFT JOIN manga ON history.manga_id = manga.manga_id " + - "WHERE history.deleted_at = 0 GROUP BY history.manga_id ORDER BY ", + "WHERE history.deleted_at = 0", ) + for (option in filterOptions) { + append(" AND ") + append(option.getCondition()) + } + append(" GROUP BY history.manga_id ORDER BY ") append(orderBy) if (limit > 0) { append(" LIMIT ") @@ -147,4 +157,11 @@ abstract class HistoryDao { @Transaction @RawQuery(observedEntities = [HistoryEntity::class]) protected abstract fun observeAllImpl(query: SupportSQLiteQuery): Flow> + + private fun ListFilterOption.getCondition(): String = when (this) { + ListFilterOption.NEW_CHAPTERS -> "(SELECT chapters_new FROM tracks WHERE tracks.manga_id = history.manga_id) > 0" + ListFilterOption.FAVORITE -> "EXISTS(SELECT * FROM favourites WHERE history.manga_id = favourites.manga_id)" + ListFilterOption.COMPLETED -> "percent >= 0.9999" + ListFilterOption.DOWNLOADED -> throw IllegalArgumentException("Unsupported option $this") + } } 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 f9a85f9df..c745858eb 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 @@ -23,6 +23,7 @@ import org.koitharu.kotatsu.core.prefs.ProgressIndicatorMode import org.koitharu.kotatsu.core.ui.util.ReversibleHandle import org.koitharu.kotatsu.core.util.ext.mapItems import org.koitharu.kotatsu.history.domain.model.MangaWithHistory +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 @@ -76,8 +77,12 @@ class HistoryRepository @Inject constructor( } } - fun observeAllWithHistory(order: ListSortOrder, limit: Int): Flow> { - return db.getHistoryDao().observeAll(order, limit).mapItems { + fun observeAllWithHistory( + order: ListSortOrder, + filterOptions: Set, + limit: Int + ): Flow> { + return db.getHistoryDao().observeAll(order, filterOptions, limit).mapItems { MangaWithHistory( it.manga.toManga(it.tags.toMangaTags()), it.history.toMangaHistory(), diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt index 03c2ee975..feadf3018 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListFragment.kt @@ -16,6 +16,7 @@ import org.koitharu.kotatsu.core.ui.util.MenuInvalidator import org.koitharu.kotatsu.core.util.ext.addMenuProvider import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.databinding.FragmentListBinding +import org.koitharu.kotatsu.list.domain.ListFilterOption import org.koitharu.kotatsu.list.ui.MangaListFragment import org.koitharu.kotatsu.list.ui.size.DynamicItemSizeResolver @@ -34,6 +35,10 @@ class HistoryListFragment : MangaListFragment() { override fun onScrolledToEnd() = viewModel.requestMoreItems() + override fun onFilterOptionClick(option: ListFilterOption) { + viewModel.onFilterOptionClick(option) + } + override fun onEmptyActionClick() { startActivity(NetworkManageIntent()) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt index 6485c98f3..b027e9f81 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt @@ -21,13 +21,16 @@ import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.core.prefs.observeAsFlow import org.koitharu.kotatsu.core.prefs.observeAsStateFlow import org.koitharu.kotatsu.core.ui.util.ReversibleAction +import org.koitharu.kotatsu.core.ui.widgets.ChipsView import org.koitharu.kotatsu.core.util.ext.calculateTimeAgo import org.koitharu.kotatsu.core.util.ext.call +import org.koitharu.kotatsu.core.util.ext.combine import org.koitharu.kotatsu.core.util.ext.onFirst import org.koitharu.kotatsu.download.ui.worker.DownloadWorker import org.koitharu.kotatsu.history.data.HistoryRepository import org.koitharu.kotatsu.history.domain.MarkAsReadUseCase import org.koitharu.kotatsu.history.domain.model.MangaWithHistory +import org.koitharu.kotatsu.list.domain.ListFilterOption import org.koitharu.kotatsu.list.domain.ListSortOrder import org.koitharu.kotatsu.list.domain.MangaListMapper import org.koitharu.kotatsu.list.ui.MangaListViewModel @@ -36,11 +39,13 @@ import org.koitharu.kotatsu.list.ui.model.EmptyState import org.koitharu.kotatsu.list.ui.model.ListHeader import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.LoadingState +import org.koitharu.kotatsu.list.ui.model.QuickFilter import org.koitharu.kotatsu.list.ui.model.TipModel import org.koitharu.kotatsu.list.ui.model.toErrorState import org.koitharu.kotatsu.local.data.LocalMangaRepository import org.koitharu.kotatsu.parsers.model.Manga import java.time.Instant +import java.util.EnumSet import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject @@ -63,6 +68,8 @@ class HistoryListViewModel @Inject constructor( valueProducer = { historySortOrder }, ) + private val filterOptions = MutableStateFlow>(EnumSet.noneOf(ListFilterOption::class.java)) + override val listMode = settings.observeAsStateFlow( scope = viewModelScope + Dispatchers.Default, key = AppSettings.KEY_LIST_MODE_HISTORY, @@ -86,25 +93,25 @@ class HistoryListViewModel @Inject constructor( ) override val content = combine( + filterOptions, observeHistory(), isGroupingEnabled, observeListModeWithTriggers(), networkState, settings.observeAsFlow(AppSettings.KEY_INCOGNITO_MODE) { isIncognitoModeEnabled }, - ) { list, grouped, mode, online, incognito -> + ) { filters, list, grouped, mode, online, incognito -> when { - list.isEmpty() -> listOf( - EmptyState( - icon = R.drawable.ic_empty_history, - textPrimary = R.string.text_history_holder_primary, - textSecondary = R.string.text_history_holder_secondary, - actionStringRes = 0, - ), - ) + list.isEmpty() -> { + if (filters.isEmpty()) { + listOf(getEmptyState(hasFilters = false)) + } else { + listOf(filterItem(filters), getEmptyState(hasFilters = true)) + } + } else -> { isReady.set(true) - mapList(list, grouped, mode, online, incognito) + mapList(filters, list, grouped, mode, online, incognito) } } }.onStart { @@ -154,17 +161,29 @@ class HistoryListViewModel @Inject constructor( } } - private fun observeHistory() = combine(sortOrder, limit, ::Pair) - .flatMapLatest { repository.observeAllWithHistory(it.first, it.second) } + fun onFilterOptionClick(option: ListFilterOption) { + filterOptions.value = EnumSet.copyOf(filterOptions.value).also { + if (option in it) { + it.remove(option) + } else { + it.add(option) + } + } + } + + private fun observeHistory() = combine(sortOrder, filterOptions, limit, ::Triple) + .flatMapLatest { repository.observeAllWithHistory(it.first, it.second - ListFilterOption.DOWNLOADED, it.third) } private suspend fun mapList( + filters: Set, list: List, grouped: Boolean, mode: ListMode, isOnline: Boolean, isIncognito: Boolean, ): List { - val result = ArrayList(if (grouped) (list.size * 1.4).toInt() else list.size + 2) + val result = ArrayList((if (grouped) (list.size * 1.4).toInt() else list.size) + 3) + result += filterItem(filters) if (isIncognito) { result += TipModel( key = AppSettings.KEY_INCOGNITO_MODE, @@ -185,12 +204,14 @@ class HistoryListViewModel @Inject constructor( actionStringRes = R.string.manage, ) } + var isEmpty = true for ((m, history) in list) { - val manga = if (!isOnline && !m.isLocal) { + val manga = if ((!isOnline && !m.isLocal) || ListFilterOption.DOWNLOADED in filters) { localMangaRepository.findSavedManga(m)?.manga ?: continue } else { m } + isEmpty = false if (grouped) { val header = history.header(order) if (header != prevHeader) { @@ -202,6 +223,9 @@ class HistoryListViewModel @Inject constructor( } result += mangaListMapper.toListModel(manga, mode) } + if (filters.isNotEmpty() && isEmpty) { + result += getEmptyState(hasFilters = true) + } return result } @@ -229,4 +253,32 @@ class HistoryListViewModel @Inject constructor( ListSortOrder.UPDATED, ListSortOrder.RATING -> null } + + private fun filterItem(selected: Set) = QuickFilter( + items = ListFilterOption.HISTORY.map { option -> + ChipsView.ChipModel( + titleResId = option.titleResId, + icon = option.iconResId, + isCheckable = true, + isChecked = option in selected, + data = option, + ) + }, + ) + + private fun getEmptyState(hasFilters: Boolean) = if (hasFilters) { + EmptyState( + icon = R.drawable.ic_empty_history, + textPrimary = R.string.nothing_found, + textSecondary = R.string.text_history_holder_secondary_filtered, + actionStringRes = 0, + ) + } else { + EmptyState( + icon = R.drawable.ic_empty_history, + textPrimary = R.string.text_history_holder_primary, + textSecondary = R.string.text_history_holder_secondary, + actionStringRes = 0, + ) + } } 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 new file mode 100644 index 000000000..5d138bf01 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/domain/ListFilterOption.kt @@ -0,0 +1,28 @@ +package org.koitharu.kotatsu.list.domain + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import org.koitharu.kotatsu.R +import java.util.EnumSet + +enum class ListFilterOption( + @StringRes val titleResId: Int, + @DrawableRes val iconResId: Int, +) { + + DOWNLOADED(R.string.on_device, R.drawable.ic_storage), + COMPLETED(R.string.status_completed, R.drawable.ic_state_finished), + NEW_CHAPTERS(R.string.new_chapters, R.drawable.ic_updated), + FAVORITE(R.string.favourites, R.drawable.ic_heart_outline), + ; + + companion object { + + val HISTORY: Set = EnumSet.of( + DOWNLOADED, + NEW_CHAPTERS, + FAVORITE, + COMPLETED, + ) + } +} 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 03d46fe2c..e2cb67a02 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 @@ -44,6 +44,7 @@ import org.koitharu.kotatsu.databinding.FragmentListBinding import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver import org.koitharu.kotatsu.favourites.ui.categories.select.FavoriteSheet +import org.koitharu.kotatsu.list.domain.ListFilterOption import org.koitharu.kotatsu.list.ui.adapter.ListItemType import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter import org.koitharu.kotatsu.list.ui.adapter.MangaListListener @@ -226,6 +227,8 @@ abstract class MangaListFragment : } } + override fun onFilterOptionClick(option: ListFilterOption) = Unit + override fun onFilterClick(view: View?) = Unit override fun onEmptyActionClick() = Unit diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ListItemType.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ListItemType.kt index b5d9eeae1..5ced570e7 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ListItemType.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ListItemType.kt @@ -2,6 +2,7 @@ package org.koitharu.kotatsu.list.ui.adapter enum class ListItemType { + FILTER_HEADER, FILTER_SORT, FILTER_TAG, FILTER_TAG_MULTI, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt index 4719a42a4..4589d3467 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt @@ -24,6 +24,7 @@ open class MangaListAdapter( addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, lifecycleOwner, listener)) addDelegate(ListItemType.HINT_EMPTY, emptyHintAD(coil, lifecycleOwner, listener)) addDelegate(ListItemType.HEADER, listHeaderAD(listener)) + addDelegate(ListItemType.FILTER_HEADER, quickFilterAD(listener)) addDelegate(ListItemType.TIP, tipAD(listener)) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt index aa8449e47..05dac38a8 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt @@ -52,7 +52,7 @@ fun mangaListDetailedItemAD( source(item.source) enqueueWith(coil) } - binding.textViewTags.text = item.tags.joinToString(separator = ", ") { it.title } + binding.textViewTags.text = item.tags.joinToString(separator = ", ") { it.title ?: "" } badge = itemView.bindBadge(badge, item.counter) } } 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 5da2f168e..609126836 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 @@ -5,7 +5,7 @@ import org.koitharu.kotatsu.core.ui.widgets.TipView import org.koitharu.kotatsu.parsers.model.MangaTag interface MangaListListener : MangaDetailsClickListener, ListStateHolderListener, ListHeaderClickListener, - TipView.OnButtonClickListener { + TipView.OnButtonClickListener, QuickFilterClickListener { fun onUpdateFilter(tags: Set) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/QuickFilterAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/QuickFilterAD.kt new file mode 100644 index 000000000..de5743dca --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/QuickFilterAD.kt @@ -0,0 +1,25 @@ +package org.koitharu.kotatsu.list.ui.adapter + +import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding +import org.koitharu.kotatsu.core.ui.widgets.ChipsView +import org.koitharu.kotatsu.databinding.ItemQuickFilterBinding +import org.koitharu.kotatsu.list.domain.ListFilterOption +import org.koitharu.kotatsu.list.ui.model.ListModel +import org.koitharu.kotatsu.list.ui.model.QuickFilter + +fun quickFilterAD( + listener: QuickFilterClickListener, +) = adapterDelegateViewBinding( + { layoutInflater, parent -> ItemQuickFilterBinding.inflate(layoutInflater, parent, false) } +) { + + binding.chipsTags.onChipClickListener = ChipsView.OnChipClickListener { chip, data -> + if (data is ListFilterOption) { + listener.onFilterOptionClick(data) + } + } + + bind { + binding.chipsTags.setChips(item.items) + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/QuickFilterClickListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/QuickFilterClickListener.kt new file mode 100644 index 000000000..8f447f3ef --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/QuickFilterClickListener.kt @@ -0,0 +1,8 @@ +package org.koitharu.kotatsu.list.ui.adapter + +import org.koitharu.kotatsu.list.domain.ListFilterOption + +interface QuickFilterClickListener { + + fun onFilterOptionClick(option: ListFilterOption) +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/TypedListSpacingDecoration.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/TypedListSpacingDecoration.kt index a69464c25..39e158d6d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/TypedListSpacingDecoration.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/TypedListSpacingDecoration.kt @@ -32,6 +32,7 @@ class TypedListSpacingDecoration( ListItemType.FILTER_TAG_MULTI, ListItemType.FILTER_STATE, ListItemType.FILTER_LANGUAGE, + ListItemType.FILTER_HEADER, -> outRect.set(0) ListItemType.HEADER, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/QuickFilter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/QuickFilter.kt new file mode 100644 index 000000000..fba190dbb --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/QuickFilter.kt @@ -0,0 +1,13 @@ +package org.koitharu.kotatsu.list.ui.model + +import org.koitharu.kotatsu.core.ui.widgets.ChipsView +import org.koitharu.kotatsu.list.ui.ListModelDiffCallback + +data class QuickFilter( + val items: List, +) : ListModel { + + override fun areItemsTheSame(other: ListModel): Boolean = other is QuickFilter + + override fun getChangePayload(previousState: ListModel) = ListModelDiffCallback.PAYLOAD_NESTED_LIST_CHANGED +} 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 2e4ecf344..e121b274c 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 @@ -27,6 +27,7 @@ import org.koitharu.kotatsu.databinding.ActivitySearchMultiBinding import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver import org.koitharu.kotatsu.favourites.ui.categories.select.FavoriteSheet +import org.koitharu.kotatsu.list.domain.ListFilterOption import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration import org.koitharu.kotatsu.list.ui.adapter.MangaListListener import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration @@ -133,6 +134,8 @@ class MultiSearchActivity : viewModel.retry() } + override fun onFilterOptionClick(option: ListFilterOption) = Unit + override fun onUpdateFilter(tags: Set) = Unit override fun onFilterClick(view: View?) = Unit 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 6d16293ff..6e9e112d8 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 @@ -24,6 +24,7 @@ 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.details.ui.DetailsActivity +import org.koitharu.kotatsu.list.domain.ListFilterOption import org.koitharu.kotatsu.list.ui.adapter.MangaListListener import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration import org.koitharu.kotatsu.list.ui.model.ListHeader @@ -94,6 +95,8 @@ class FeedFragment : viewModel.update() } + override fun onFilterOptionClick(option: ListFilterOption) = Unit + override fun onRetryClick(error: Throwable) = Unit override fun onUpdateFilter(tags: Set) = Unit diff --git a/app/src/main/res/layout/item_quick_filter.xml b/app/src/main/res/layout/item_quick_filter.xml new file mode 100644 index 000000000..fa301a8d5 --- /dev/null +++ b/app/src/main/res/layout/item_quick_filter.xml @@ -0,0 +1,24 @@ + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a3ed043af..c7b6b25a6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -96,6 +96,7 @@ Try to reformulate the query. What you read will be displayed here Find what to read in the «Explore» section + There are no manga matching the filters you selected Save something first Save something from an online catalog or import it from a file. Shelf