From 8291c55fc9510a3159eccf369f3f27f4ac51e9d1 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 19 Apr 2025 08:16:08 +0300 Subject: [PATCH 1/9] Fix some database-related crashes --- .../details/ui/pager/ChaptersPagesViewModel.kt | 6 +++++- .../org/koitharu/kotatsu/tracker/data/TracksDao.kt | 12 ++++++------ .../kotatsu/tracker/domain/TrackingRepository.kt | 7 +++---- .../tracker/ui/debug/TrackerDebugViewModel.kt | 1 + 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/ChaptersPagesViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/ChaptersPagesViewModel.kt index 3643a4c9e..2e6979c10 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/ChaptersPagesViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/ChaptersPagesViewModel.kt @@ -100,7 +100,11 @@ abstract class ChaptersPagesViewModel( }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false) val bookmarks = mangaDetails.flatMapLatest { - if (it != null) bookmarksRepository.observeBookmarks(it.toManga()) else flowOf(emptyList()) + if (it != null) { + bookmarksRepository.observeBookmarks(it.toManga()).withErrorHandling() + } else { + flowOf(emptyList()) + } }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, emptyList()) val chapters = combine( diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/TracksDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/TracksDao.kt index 07f04a241..009e77f00 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/TracksDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/TracksDao.kt @@ -27,17 +27,17 @@ abstract class TracksDao : MangaQueryBuilder.ConditionCallback { @Query("SELECT * FROM tracks WHERE manga_id = :mangaId") abstract suspend fun find(mangaId: Long): TrackEntity? - @Query("SELECT chapters_new FROM tracks WHERE manga_id = :mangaId") - abstract suspend fun findNewChapters(mangaId: Long): Int? + @Query("SELECT IFNULL(chapters_new,0) FROM tracks WHERE manga_id = :mangaId") + abstract suspend fun findNewChapters(mangaId: Long): Int @Query("SELECT COUNT(*) FROM tracks") abstract suspend fun getTracksCount(): Int - @Query("SELECT chapters_new FROM tracks") - abstract fun observeNewChapters(): Flow> + @Query("SELECT COUNT(*) FROM tracks WHERE chapters_new > 0") + abstract fun observeUpdateMangaCount(): Flow - @Query("SELECT chapters_new FROM tracks WHERE manga_id = :mangaId") - abstract fun observeNewChapters(mangaId: Long): Flow + @Query("SELECT IFNULL(chapters_new, 0) FROM tracks WHERE manga_id = :mangaId") + abstract fun observeNewChapters(mangaId: Long): Flow @Transaction @Query("SELECT * FROM tracks WHERE chapters_new > 0 ORDER BY last_chapter_date DESC") diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt index dfe81770f..b2becc7ae 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt @@ -5,7 +5,6 @@ import androidx.room.withTransaction import dagger.Reusable import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.entity.toManga @@ -39,16 +38,16 @@ class TrackingRepository @Inject constructor( private var isGcCalled = AtomicBoolean(false) suspend fun getNewChaptersCount(mangaId: Long): Int { - return db.getTracksDao().findNewChapters(mangaId) ?: 0 + return db.getTracksDao().findNewChapters(mangaId) } fun observeNewChaptersCount(mangaId: Long): Flow { - return db.getTracksDao().observeNewChapters(mangaId).map { it ?: 0 } + return db.getTracksDao().observeNewChapters(mangaId) } @Deprecated("") fun observeUpdatedMangaCount(): Flow { - return db.getTracksDao().observeNewChapters().map { list -> list.count { it > 0 } } + return db.getTracksDao().observeUpdateMangaCount() .onStart { gcIfNotCalled() } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/debug/TrackerDebugViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/debug/TrackerDebugViewModel.kt index a995979b0..8c3f721f6 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/debug/TrackerDebugViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/debug/TrackerDebugViewModel.kt @@ -21,6 +21,7 @@ class TrackerDebugViewModel @Inject constructor( val content = db.getTracksDao().observeAll() .map { it.toUiList() } + .withErrorHandling() .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList()) private fun List.toUiList(): List = map { From 14b89fbee27c8daef81d36f0383f0534ae2f05a3 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 19 Apr 2025 08:31:47 +0300 Subject: [PATCH 2/9] Use pagination for bookmarks backup --- .../kotatsu/bookmarks/data/BookmarksDao.kt | 4 +-- .../kotatsu/core/backup/BackupRepository.kt | 35 +++++++++++-------- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/data/BookmarksDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/data/BookmarksDao.kt index d701edaf1..6fa5360dc 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/data/BookmarksDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/data/BookmarksDao.kt @@ -17,9 +17,9 @@ abstract class BookmarksDao { @Transaction @Query( - "SELECT * FROM manga JOIN bookmarks ON bookmarks.manga_id = manga.manga_id ORDER BY percent", + "SELECT * FROM manga JOIN bookmarks ON bookmarks.manga_id = manga.manga_id ORDER BY percent LIMIT :limit OFFSET :offset", ) - abstract suspend fun findAll(): Map> + abstract suspend fun findAll(offset: Int, limit: Int): Map> @Query("SELECT * FROM bookmarks WHERE manga_id = :mangaId AND chapter_id = :chapterId AND page = :page ORDER BY percent") abstract fun observe(mangaId: Long, chapterId: Long, page: Int): Flow diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/BackupRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/BackupRepository.kt index 5a63fd5aa..c1f30c0d1 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/BackupRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/BackupRepository.kt @@ -28,7 +28,7 @@ class BackupRepository @Inject constructor( var offset = 0 val entry = BackupEntry(BackupEntry.Name.HISTORY, JSONArray()) while (true) { - val history = db.getHistoryDao().findAll(offset, PAGE_SIZE) + val history = db.getHistoryDao().findAll(offset = offset, limit = PAGE_SIZE) if (history.isEmpty()) { break } @@ -59,7 +59,7 @@ class BackupRepository @Inject constructor( var offset = 0 val entry = BackupEntry(BackupEntry.Name.FAVOURITES, JSONArray()) while (true) { - val favourites = db.getFavouritesDao().findAllRaw(offset, PAGE_SIZE) + val favourites = db.getFavouritesDao().findAllRaw(offset = offset, limit = PAGE_SIZE) if (favourites.isEmpty()) { break } @@ -78,19 +78,26 @@ class BackupRepository @Inject constructor( } suspend fun dumpBookmarks(): BackupEntry { + var offset = 0 val entry = BackupEntry(BackupEntry.Name.BOOKMARKS, JSONArray()) - val all = db.getBookmarksDao().findAll() - for ((m, b) in all) { - val json = JSONObject() - val manga = JsonSerializer(m.manga).toJson() - json.put("manga", manga) - val tags = JSONArray() - m.tags.forEach { tags.put(JsonSerializer(it).toJson()) } - json.put("tags", tags) - val bookmarks = JSONArray() - b.forEach { bookmarks.put(JsonSerializer(it).toJson()) } - json.put("bookmarks", bookmarks) - entry.data.put(json) + while (true) { + val bookmarks = db.getBookmarksDao().findAll(offset = offset, limit = PAGE_SIZE) + if (bookmarks.isEmpty()) { + break + } + offset += bookmarks.size + for ((m, b) in bookmarks) { + val json = JSONObject() + val manga = JsonSerializer(m.manga).toJson() + json.put("manga", manga) + val tags = JSONArray() + m.tags.forEach { tags.put(JsonSerializer(it).toJson()) } + json.put("tags", tags) + val bookmarks = JSONArray() + b.forEach { bookmarks.put(JsonSerializer(it).toJson()) } + json.put("bookmarks", bookmarks) + entry.data.put(json) + } } return entry } From 4fb1db47ab08f12cf00b3758e0e637484bf8458f Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 30 Apr 2025 17:27:41 +0300 Subject: [PATCH 3/9] Fix image loading (cherry picked from commit 257f583f78e1bacf7856e7819979f7bb7e9fda06) --- .../kotatsu/core/image/AvifImageDecoder.kt | 6 ++--- .../kotatsu/core/image/BitmapDecoderCompat.kt | 5 +++- .../koitharu/kotatsu/core/image/CbzFetcher.kt | 2 +- .../kotatsu/core/image/RegionBitmapDecoder.kt | 26 ++++++++++++------- .../koitharu/kotatsu/core/util/ext/Coil.kt | 15 +++++++++++ .../org/koitharu/kotatsu/core/util/ext/IO.kt | 9 +++++++ 6 files changed, 47 insertions(+), 16 deletions(-) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/image/AvifImageDecoder.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/image/AvifImageDecoder.kt index 65ea8edd4..4cb017e3d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/image/AvifImageDecoder.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/image/AvifImageDecoder.kt @@ -13,7 +13,7 @@ import com.davemorrissey.labs.subscaleview.decoder.ImageDecodeException import kotlinx.coroutines.runInterruptible import org.aomedia.avif.android.AvifDecoder import org.aomedia.avif.android.AvifDecoder.Info -import org.koitharu.kotatsu.core.util.ext.toByteBuffer +import org.koitharu.kotatsu.core.util.ext.readByteBuffer class AvifImageDecoder( private val source: ImageSource, @@ -21,9 +21,7 @@ class AvifImageDecoder( ) : Decoder { override suspend fun decode(): DecodeResult = runInterruptible { - val bytes = source.source().use { - it.inputStream().toByteBuffer() - } + val bytes = source.source().readByteBuffer() val info = Info() if (!AvifDecoder.getInfo(bytes, bytes.remaining(), info)) { throw ImageDecodeException( diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/image/BitmapDecoderCompat.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/image/BitmapDecoderCompat.kt index 4bedf2f48..b84d31a77 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/image/BitmapDecoderCompat.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/image/BitmapDecoderCompat.kt @@ -9,12 +9,15 @@ import androidx.annotation.RequiresApi import androidx.core.graphics.createBitmap import com.davemorrissey.labs.subscaleview.decoder.ImageDecodeException import okio.IOException +import okio.buffer +import okio.source import org.aomedia.avif.android.AvifDecoder import org.aomedia.avif.android.AvifDecoder.Info import org.jetbrains.annotations.Blocking import org.koitharu.kotatsu.core.util.MimeTypes import org.koitharu.kotatsu.core.util.ext.MimeType import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug +import org.koitharu.kotatsu.core.util.ext.readByteBuffer import org.koitharu.kotatsu.core.util.ext.toByteBuffer import org.koitharu.kotatsu.core.util.ext.toMimeTypeOrNull import org.koitharu.kotatsu.parsers.util.runCatchingCancellable @@ -28,7 +31,7 @@ object BitmapDecoderCompat { @Blocking fun decode(file: File): Bitmap = when (val format = probeMimeType(file)?.subtype) { - FORMAT_AVIF -> file.inputStream().use { decodeAvif(it.toByteBuffer()) } + FORMAT_AVIF -> file.source().buffer().use { decodeAvif(it.readByteBuffer()) } else -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { ImageDecoder.decodeBitmap(ImageDecoder.createSource(file)) } else { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/image/CbzFetcher.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/image/CbzFetcher.kt index c2a2561fd..d4a59ae12 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/image/CbzFetcher.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/image/CbzFetcher.kt @@ -25,7 +25,7 @@ class CbzFetcher( val entryName = requireNotNull(uri.fragment) val fs = options.fileSystem.openZip(filePath) SourceFetchResult( - source = ImageSource(entryName.toPath(), fs, closeable = fs), + source = ImageSource(entryName.toPath(), fs), mimeType = MimeTypes.getMimeTypeFromExtension(entryName)?.toString(), dataSource = DataSource.DISK, ) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/image/RegionBitmapDecoder.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/image/RegionBitmapDecoder.kt index abee08347..c1989ac61 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/image/RegionBitmapDecoder.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/image/RegionBitmapDecoder.kt @@ -23,6 +23,7 @@ import coil3.size.Scale import coil3.size.Size import coil3.size.isOriginal import coil3.size.pxOrElse +import org.koitharu.kotatsu.core.util.ext.copyWithNewSource import kotlin.math.roundToInt class RegionBitmapDecoder( @@ -34,16 +35,21 @@ class RegionBitmapDecoder( override suspend fun decode(): DecodeResult? { val regionDecoder = BitmapDecoderCompat.createRegionDecoder(fetchResult.source.source().inputStream()) if (regionDecoder == null) { - val fallbackDecoder = imageLoader.components.newDecoder( - result = fetchResult, - options = options, - imageLoader = imageLoader, - startIndex = 0, - )?.first - return if (fallbackDecoder == null || fallbackDecoder is RegionBitmapDecoder) { - null - } else { - fallbackDecoder.decode() + val revivedFetchResult = fetchResult.copyWithNewSource() + return try { + val fallbackDecoder = imageLoader.components.newDecoder( + result = revivedFetchResult, + options = options, + imageLoader = imageLoader, + startIndex = 0, + )?.first + if (fallbackDecoder == null || fallbackDecoder is RegionBitmapDecoder) { + null + } else { + fallbackDecoder.decode() + } + } finally { + revivedFetchResult.source.close() } } val bitmapOptions = BitmapFactory.Options() diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Coil.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Coil.kt index 4256bae0b..e1ca78ab1 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Coil.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Coil.kt @@ -6,10 +6,13 @@ import android.widget.ImageView import androidx.core.graphics.ColorUtils import androidx.core.graphics.drawable.toDrawable import androidx.lifecycle.LifecycleOwner +import androidx.annotation.CheckResult import coil3.Extras import coil3.ImageLoader import coil3.asDrawable +import coil3.decode.ImageSource import coil3.fetch.FetchResult +import coil3.fetch.SourceFetchResult import coil3.request.ErrorResult import coil3.request.ImageRequest import coil3.request.ImageResult @@ -28,6 +31,7 @@ import coil3.toBitmap import coil3.util.CoilUtils import com.google.android.material.progressindicator.BaseProgressIndicator import org.koitharu.kotatsu.R +import okio.buffer import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.core.image.RegionBitmapDecoder import org.koitharu.kotatsu.core.ui.image.AnimatedPlaceholderDrawable @@ -163,3 +167,14 @@ private class CompositeImageRequestListener( val mangaKey = Extras.Key(null) val bookmarkKey = Extras.Key(null) val mangaSourceKey = Extras.Key(null) + +@CheckResult +fun SourceFetchResult.copyWithNewSource(): SourceFetchResult = SourceFetchResult( + source = ImageSource( + source = source.fileSystem.source(source.file()).buffer(), + fileSystem = source.fileSystem, + metadata = source.metadata, + ), + mimeType = mimeType, + dataSource = dataSource, +) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/IO.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/IO.kt index 8cce14a33..3882949e8 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/IO.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/IO.kt @@ -7,6 +7,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.withContext import okhttp3.ResponseBody import okio.BufferedSink +import okio.BufferedSource import okio.FileSystem import okio.IOException import okio.Path @@ -30,6 +31,14 @@ suspend fun BufferedSink.writeAllCancellable(source: Source) = withContext(Dispa writeAll(source.cancellable()) } +fun BufferedSource.readByteBuffer(): ByteBuffer { + val bytes = readByteArray() + return ByteBuffer.allocateDirect(bytes.size) + .put(bytes) + .rewind() as ByteBuffer +} + +@Deprecated("") fun InputStream.toByteBuffer(): ByteBuffer { val outStream = ByteArrayOutputStream(available()) copyTo(outStream) From 9cf496b7c41e0a25f498feda626421f4166c293f Mon Sep 17 00:00:00 2001 From: Koitharu Date: Fri, 2 May 2025 14:38:12 +0300 Subject: [PATCH 4/9] AVIF images downsampling (cherry picked from commit 5d890cb3d0ee16a77d02102e8dd87a89ff7944e3) --- .../kotatsu/core/image/AvifImageDecoder.kt | 67 ++++++++++++++----- 1 file changed, 49 insertions(+), 18 deletions(-) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/image/AvifImageDecoder.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/image/AvifImageDecoder.kt index 4cb017e3d..29c7ee701 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/image/AvifImageDecoder.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/image/AvifImageDecoder.kt @@ -2,17 +2,21 @@ package org.koitharu.kotatsu.core.image import android.graphics.Bitmap import androidx.core.graphics.createBitmap +import androidx.core.graphics.scale import coil3.ImageLoader import coil3.asImage import coil3.decode.DecodeResult +import coil3.decode.DecodeUtils import coil3.decode.Decoder import coil3.decode.ImageSource import coil3.fetch.SourceFetchResult import coil3.request.Options +import coil3.request.maxBitmapSize +import coil3.util.component1 +import coil3.util.component2 import com.davemorrissey.labs.subscaleview.decoder.ImageDecodeException import kotlinx.coroutines.runInterruptible import org.aomedia.avif.android.AvifDecoder -import org.aomedia.avif.android.AvifDecoder.Info import org.koitharu.kotatsu.core.util.ext.readByteBuffer class AvifImageDecoder( @@ -22,24 +26,51 @@ class AvifImageDecoder( override suspend fun decode(): DecodeResult = runInterruptible { val bytes = source.source().readByteBuffer() - val info = Info() - if (!AvifDecoder.getInfo(bytes, bytes.remaining(), info)) { - throw ImageDecodeException( - null, - "avif", - "Requested to decode byte buffer which cannot be handled by AvifDecoder", - ) - } - val config = if (info.depth == 8 || info.alphaPresent) Bitmap.Config.ARGB_8888 else Bitmap.Config.RGB_565 - val bitmap = createBitmap(info.width, info.height, config) - if (!AvifDecoder.decode(bytes, bytes.remaining(), bitmap)) { - bitmap.recycle() - throw ImageDecodeException(null, "avif") - } - DecodeResult( - image = bitmap.asImage(), - isSampled = false, + val decoder = AvifDecoder.create(bytes) ?: throw ImageDecodeException( + uri = source.fileOrNull()?.toString(), + format = "avif", + message = "Requested to decode byte buffer which cannot be handled by AvifDecoder", ) + try { + val config = if (decoder.depth == 8 || decoder.alphaPresent) { + Bitmap.Config.ARGB_8888 + } else { + Bitmap.Config.RGB_565 + } + val bitmap = createBitmap(decoder.width, decoder.height, config) + val result = decoder.nextFrame(bitmap) + if (result != 0) { + bitmap.recycle() + throw ImageDecodeException( + uri = source.fileOrNull()?.toString(), + format = "avif", + message = AvifDecoder.resultToString(result), + ) + } + // downscaling + val (dstWidth, dstHeight) = DecodeUtils.computeDstSize( + srcWidth = bitmap.width, + srcHeight = bitmap.height, + targetSize = options.size, + scale = options.scale, + maxSize = options.maxBitmapSize, + ) + if (dstWidth < bitmap.width || dstHeight < bitmap.height) { + val scaled = bitmap.scale(dstWidth, dstHeight) + bitmap.recycle() + DecodeResult( + image = scaled.asImage(), + isSampled = true, + ) + } else { + DecodeResult( + image = bitmap.asImage(), + isSampled = false, + ) + } + } finally { + decoder.release() + } } class Factory : Decoder.Factory { From 4449996a91bd25abb46390dad483f6a8d9c97daf Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 27 Apr 2025 15:28:48 +0300 Subject: [PATCH 5/9] Fix search suggestions (cherry picked from commit 1a8045b89f04f94f0475e5025a866c7ed440215c) --- .../search/domain/MangaSearchRepository.kt | 2 +- .../ui/suggestion/SearchSuggestionFragment.kt | 17 +- .../suggestion/SearchSuggestionViewModel.kt | 153 ++++++++++++------ .../adapter/SearchSuggestionAdapter.kt | 1 + .../adapter/SearchSuggestionTextAD.kt | 28 ++++ .../suggestion/model/SearchSuggestionItem.kt | 12 ++ .../kotatsu/suggestions/data/SuggestionDao.kt | 24 ++- .../domain/SuggestionRepository.kt | 7 +- .../layout/item_search_suggestion_text.xml | 14 ++ 9 files changed, 185 insertions(+), 73 deletions(-) create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionTextAD.kt create mode 100644 app/src/main/res/layout/item_search_suggestion_text.xml diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt index 924334415..75ed86995 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt @@ -37,7 +37,7 @@ class MangaSearchRepository @Inject constructor( suspend fun getMangaSuggestion(query: String, limit: Int, source: MangaSource?): List { return when { - query.isEmpty() -> db.getSuggestionDao().getRandom(limit).map { MangaWithTags(it.manga, it.tags) } + query.isEmpty() -> db.getSuggestionDao().getRandom(limit).map { MangaWithTags(it.manga, emptyList()) } source != null -> db.getMangaDao().searchByTitle("%$query%", source.name, limit) else -> db.getMangaDao().searchByTitle("%$query%", limit) }.let { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionFragment.kt index 08bc21bdf..126a1a3d7 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionFragment.kt @@ -9,11 +9,13 @@ import androidx.fragment.app.activityViewModels import androidx.recyclerview.widget.ItemTouchHelper import coil3.ImageLoader import dagger.hilt.android.AndroidEntryPoint +import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver import org.koitharu.kotatsu.core.os.VoiceInputContract import org.koitharu.kotatsu.core.ui.BaseFragment import org.koitharu.kotatsu.core.util.ext.addMenuProvider -import org.koitharu.kotatsu.core.util.ext.consumeAllSystemBarsInsets +import org.koitharu.kotatsu.core.util.ext.consumeAll import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.databinding.FragmentSearchSuggestionBinding import org.koitharu.kotatsu.search.ui.suggestion.adapter.SearchSuggestionAdapter import javax.inject.Inject @@ -49,19 +51,16 @@ class SearchSuggestionFragment : binding.root.adapter = adapter binding.root.setHasFixedSize(true) viewModel.suggestion.observe(viewLifecycleOwner, adapter) + viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.root, this)) ItemTouchHelper(SearchSuggestionItemCallback(this)) .attachToRecyclerView(binding.root) } override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat { - val barsInsets = insets.getInsets(WindowInsetsCompat.Type.ime() or WindowInsetsCompat.Type.systemBars()) - v.setPadding( - barsInsets.left, - 0, - barsInsets.right, - barsInsets.bottom, - ) - return insets.consumeAllSystemBarsInsets() + val typeMask = WindowInsetsCompat.Type.ime() or WindowInsetsCompat.Type.systemBars() + val barsInsets = insets.getInsets(typeMask) + v.setPadding(barsInsets.left, 0, barsInsets.right, barsInsets.bottom) + return insets.consumeAll(typeMask) } override fun onRemoveQuery(query: String) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionViewModel.kt index 28b1f4125..d4c779413 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionViewModel.kt @@ -21,11 +21,12 @@ import org.koitharu.kotatsu.core.prefs.observeAsFlow import org.koitharu.kotatsu.core.prefs.observeAsStateFlow import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.ui.widgets.ChipsView +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.explore.data.MangaSourcesRepository import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.util.mapToSet -import org.koitharu.kotatsu.parsers.util.sizeOrZero +import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.search.domain.MangaSearchRepository import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem import javax.inject.Inject @@ -87,7 +88,7 @@ class SearchSuggestionViewModel @Inject constructor( } fun onResume() { - if (invalidateOnResume) { + if (invalidateOnResume || suggestionJob?.isActive != true) { invalidateOnResume = false setupSuggestion() } @@ -120,62 +121,114 @@ class SearchSuggestionViewModel @Inject constructor( enabledSources: Set, types: Set, ): List = coroutineScope { - val queriesDeferred = if (SearchSuggestionType.QUERIES_RECENT in types) { - async { repository.getQuerySuggestion(searchQuery, MAX_QUERY_ITEMS) } + listOfNotNull( + if (SearchSuggestionType.GENRES in types) { + async { getTags(searchQuery) } + } else { + null + }, + if (SearchSuggestionType.MANGA in types) { + async { getManga(searchQuery) } + } else { + null + }, + if (SearchSuggestionType.QUERIES_RECENT in types) { + async { getRecentQueries(searchQuery) } + } else { + null + }, + if (SearchSuggestionType.QUERIES_SUGGEST in types) { + async { getQueryHints(searchQuery) } + } else { + null + }, + if (SearchSuggestionType.SOURCES in types) { + async { getSources(searchQuery, enabledSources) } + } else { + null + }, + if (SearchSuggestionType.RECENT_SOURCES in types) { + async { getRecentSources(searchQuery) } + } else { + null + }, + if (SearchSuggestionType.AUTHORS in types) { + async { + getAuthors(searchQuery) + } + } else { + null + }, + ).flatMap { it.await() } + } + + private suspend fun getAuthors(searchQuery: String): List = runCatchingCancellable { + repository.getAuthorsSuggestion(searchQuery, MAX_AUTHORS_ITEMS) + .map { SearchSuggestionItem.Author(it) } + }.getOrElse { e -> + e.printStackTraceDebug() + listOf(SearchSuggestionItem.Text(0, e)) + } + + private suspend fun getQueryHints(searchQuery: String): List = runCatchingCancellable { + repository.getQueryHintSuggestion(searchQuery, MAX_HINTS_ITEMS) + .map { SearchSuggestionItem.Hint(it) } + }.getOrElse { e -> + e.printStackTraceDebug() + listOf(SearchSuggestionItem.Text(0, e)) + } + + private suspend fun getRecentQueries(searchQuery: String): List = runCatchingCancellable { + repository.getQuerySuggestion(searchQuery, MAX_QUERY_ITEMS) + .map { SearchSuggestionItem.RecentQuery(it) } + }.getOrElse { e -> + e.printStackTraceDebug() + listOf(SearchSuggestionItem.Text(0, e)) + } + + private suspend fun getTags(searchQuery: String): List = runCatchingCancellable { + val tags = repository.getTagsSuggestion(searchQuery, MAX_TAGS_ITEMS, null) + if (tags.isEmpty()) { + emptyList() } else { - null + listOf(SearchSuggestionItem.Tags(mapTags(tags))) } - val hintsDeferred = if (SearchSuggestionType.QUERIES_SUGGEST in types) { - async { repository.getQueryHintSuggestion(searchQuery, MAX_HINTS_ITEMS) } + }.getOrElse { e -> + e.printStackTraceDebug() + listOf(SearchSuggestionItem.Text(0, e)) + } + + private suspend fun getManga(searchQuery: String): List = runCatchingCancellable { + val manga = repository.getMangaSuggestion(searchQuery, MAX_MANGA_ITEMS, null) + if (manga.isEmpty()) { + emptyList() } else { - null + listOf(SearchSuggestionItem.MangaList(manga)) } - val authorsDeferred = if (SearchSuggestionType.AUTHORS in types) { - async { repository.getAuthorsSuggestion(searchQuery, MAX_AUTHORS_ITEMS) } - } else { - null - } - val tagsDeferred = if (SearchSuggestionType.GENRES in types) { - async { repository.getTagsSuggestion(searchQuery, MAX_TAGS_ITEMS, null) } - } else { - null - } - val mangaDeferred = if (SearchSuggestionType.MANGA in types) { - async { repository.getMangaSuggestion(searchQuery, MAX_MANGA_ITEMS, null) } - } else { - null - } - val sources = if (SearchSuggestionType.SOURCES in types) { + }.getOrElse { e -> + e.printStackTraceDebug() + listOf(SearchSuggestionItem.Text(0, e)) + } + + private suspend fun getSources(searchQuery: String, enabledSources: Set): List = + runCatchingCancellable { repository.getSourcesSuggestion(searchQuery, MAX_SOURCES_ITEMS) - } else { - null - } - val sourcesTipsDeferred = if (searchQuery.isEmpty() && SearchSuggestionType.RECENT_SOURCES in types) { - async { repository.getSourcesSuggestion(MAX_SOURCES_TIPS_ITEMS) } - } else { - null + .map { SearchSuggestionItem.Source(it, it.name in enabledSources) } + }.getOrElse { e -> + e.printStackTraceDebug() + listOf(SearchSuggestionItem.Text(0, e)) } - val tags = tagsDeferred?.await() - val mangaList = mangaDeferred?.await() - val queries = queriesDeferred?.await() - val hints = hintsDeferred?.await() - val authors = authorsDeferred?.await() - val sourcesTips = sourcesTipsDeferred?.await() - - buildList(queries.sizeOrZero() + sources.sizeOrZero() + authors.sizeOrZero() + hints.sizeOrZero() + 2) { - if (!tags.isNullOrEmpty()) { - add(SearchSuggestionItem.Tags(mapTags(tags))) - } - if (!mangaList.isNullOrEmpty()) { - add(SearchSuggestionItem.MangaList(mangaList)) - } - sources?.mapTo(this) { SearchSuggestionItem.Source(it, it.name in enabledSources) } - queries?.mapTo(this) { SearchSuggestionItem.RecentQuery(it) } - authors?.mapTo(this) { SearchSuggestionItem.Author(it) } - hints?.mapTo(this) { SearchSuggestionItem.Hint(it) } - sourcesTips?.mapTo(this) { SearchSuggestionItem.SourceTip(it) } + private suspend fun getRecentSources(searchQuery: String): List = if (searchQuery.isEmpty()) { + runCatchingCancellable { + repository.getSourcesSuggestion(MAX_SOURCES_TIPS_ITEMS) + .map { SearchSuggestionItem.SourceTip(it) } + }.getOrElse { e -> + e.printStackTraceDebug() + listOf(SearchSuggestionItem.Text(0, e)) } + } else { + emptyList() } private fun mapTags(tags: List): List = tags.map { tag -> diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionAdapter.kt index a788c6b88..63609b77f 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionAdapter.kt @@ -23,5 +23,6 @@ class SearchSuggestionAdapter( .addDelegate(searchSuggestionMangaListAD(coil, lifecycleOwner, listener)) .addDelegate(searchSuggestionQueryHintAD(listener)) .addDelegate(searchSuggestionAuthorAD(listener)) + .addDelegate(searchSuggestionTextAD()) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionTextAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionTextAD.kt new file mode 100644 index 000000000..5b1f47978 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionTextAD.kt @@ -0,0 +1,28 @@ +package org.koitharu.kotatsu.search.ui.suggestion.adapter + +import android.widget.TextView +import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.util.ext.getDisplayMessage +import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem + +fun searchSuggestionTextAD() = adapterDelegate( + R.layout.item_search_suggestion_text, +) { + + bind { + val tv = itemView as TextView + val isError = item.error != null + tv.setCompoundDrawablesRelativeWithIntrinsicBounds( + if (isError) R.drawable.ic_error_small else 0, + 0, + 0, + 0, + ) + if (item.textResId != 0) { + tv.setText(item.textResId) + } else { + tv.text = item.error?.getDisplayMessage(tv.resources) + } + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/model/SearchSuggestionItem.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/model/SearchSuggestionItem.kt index 27f877460..a0a428f1a 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/model/SearchSuggestionItem.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/model/SearchSuggestionItem.kt @@ -1,5 +1,6 @@ package org.koitharu.kotatsu.search.ui.suggestion.model +import androidx.annotation.StringRes import org.koitharu.kotatsu.core.model.isNsfw import org.koitharu.kotatsu.core.ui.widgets.ChipsView import org.koitharu.kotatsu.list.ui.ListModelDiffCallback @@ -93,4 +94,15 @@ sealed interface SearchSuggestionItem : ListModel { return ListModelDiffCallback.PAYLOAD_NESTED_LIST_CHANGED } } + + data class Text( + @StringRes val textResId: Int, + val error: Throwable?, + ) : SearchSuggestionItem { + + override fun areItemsTheSame(other: ListModel): Boolean = other is Text + && textResId == other.textResId + && error?.javaClass == other.error?.javaClass + && error?.message == other.error?.message + } } 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 f61d88f3f..12e0eb5b3 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 @@ -11,6 +11,7 @@ import androidx.room.Update import androidx.sqlite.db.SupportSQLiteQuery import kotlinx.coroutines.flow.Flow import org.koitharu.kotatsu.core.db.MangaQueryBuilder +import org.koitharu.kotatsu.core.db.entity.MangaWithTags import org.koitharu.kotatsu.core.db.entity.TagEntity import org.koitharu.kotatsu.list.domain.ListFilterOption @@ -33,12 +34,10 @@ abstract class SuggestionDao : MangaQueryBuilder.ConditionCallback { ) @Transaction - @Query("SELECT * FROM suggestions ORDER BY RANDOM() LIMIT 1") - abstract suspend fun getRandom(): SuggestionWithManga? - - @Transaction - @Query("SELECT * FROM suggestions ORDER BY RANDOM() LIMIT :limit") - abstract suspend fun getRandom(limit: Int): List + open suspend fun getRandom(limit: Int): List { + val ids = getRandomIds(limit) + return getByIds(ids) + } @Query("SELECT COUNT(*) FROM suggestions") abstract suspend fun count(): Int @@ -68,6 +67,12 @@ abstract class SuggestionDao : MangaQueryBuilder.ConditionCallback { } } + @Query("SELECT * FROM manga WHERE manga_id IN (:ids)") + protected abstract suspend fun getByIds(ids: LongArray): List + + @Query("SELECT manga_id FROM suggestions ORDER BY RANDOM() LIMIT :limit") + protected abstract suspend fun getRandomIds(limit: Int): LongArray + @Transaction @RawQuery(observedEntities = [SuggestionEntity::class]) protected abstract fun observeAllImpl(query: SupportSQLiteQuery): Flow> @@ -75,7 +80,12 @@ 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)}" + 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 6a2e168cf..4e2cacc93 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 @@ -6,7 +6,6 @@ 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.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 @@ -34,10 +33,6 @@ class SuggestionRepository @Inject constructor( } } - suspend fun getRandom(): Manga? { - return db.getSuggestionDao().getRandom()?.toManga() - } - suspend fun getRandomList(limit: Int): List { return db.getSuggestionDao().getRandom(limit).map { it.toManga() @@ -80,5 +75,5 @@ class SuggestionRepository @Inject constructor( } } - private fun SuggestionWithManga.toManga() = manga.toManga(tags.toMangaTags(), null) + private fun SuggestionWithManga.toManga() = manga.toManga(emptySet(), null) } diff --git a/app/src/main/res/layout/item_search_suggestion_text.xml b/app/src/main/res/layout/item_search_suggestion_text.xml new file mode 100644 index 000000000..5440f9815 --- /dev/null +++ b/app/src/main/res/layout/item_search_suggestion_text.xml @@ -0,0 +1,14 @@ + + From 0983885fa201cee85695ab1d7639e1ad01b06a79 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 3 May 2025 08:30:36 +0300 Subject: [PATCH 6/9] Update private notifications visibility --- app/build.gradle | 4 ++-- .../org/koitharu/kotatsu/core/model/Manga.kt | 2 ++ .../ui/worker/DownloadNotificationFactory.kt | 7 ++++--- .../kotatsu/suggestions/ui/SuggestionsWorker.kt | 2 +- .../tracker/work/TrackerNotificationHelper.kt | 15 ++++++++++++--- 5 files changed, 21 insertions(+), 9 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 5c14ce619..9e6361cda 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -19,8 +19,8 @@ android { applicationId 'org.koitharu.kotatsu' minSdk = 21 targetSdk = 35 - versionCode = 1009 - versionName = '8.1.3' + versionCode = 1010 + versionName = '8.1.4' generatedDensities = [] testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner' ksp { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt index 10304ef2b..89723d8ba 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt @@ -149,6 +149,8 @@ fun Manga.chaptersCount(): Int { return max } +fun Manga.isNsfw(): Boolean = contentRating == ContentRating.ADULT || source.isNsfw() + fun MangaListFilter.getSummary() = buildSpannedString { if (!query.isNullOrEmpty()) { append(query) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadNotificationFactory.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadNotificationFactory.kt index 9b3d0b651..ecf6af769 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadNotificationFactory.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadNotificationFactory.kt @@ -25,6 +25,7 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.ErrorReporterReceiver import org.koitharu.kotatsu.core.LocalizedAppContext import org.koitharu.kotatsu.core.model.LocalMangaSource +import org.koitharu.kotatsu.core.model.isNsfw import org.koitharu.kotatsu.core.nav.AppRouter import org.koitharu.kotatsu.core.util.ext.getDrawableOrThrow import org.koitharu.kotatsu.core.util.ext.isReportable @@ -140,10 +141,10 @@ class DownloadNotificationFactory @AssistedInject constructor( builder.setSubText(null) builder.setShowWhen(false) builder.setVisibility( - if (state != null && state.manga.isNsfw) { - NotificationCompat.VISIBILITY_PRIVATE + if (state != null && state.manga.isNsfw()) { + NotificationCompat.VISIBILITY_SECRET } else { - NotificationCompat.VISIBILITY_PUBLIC + NotificationCompat.VISIBILITY_PRIVATE }, ) when { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt index a2c4108b6..2d8142cf8 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt @@ -352,7 +352,7 @@ class SuggestionsWorker @AssistedInject constructor( ) setAutoCancel(true) setCategory(NotificationCompat.CATEGORY_RECOMMENDATION) - setVisibility(if (manga.isNsfw) NotificationCompat.VISIBILITY_SECRET else NotificationCompat.VISIBILITY_PUBLIC) + setVisibility(if (manga.isNsfw()) NotificationCompat.VISIBILITY_SECRET else NotificationCompat.VISIBILITY_PRIVATE) setShortcutId(manga.id.toString()) priority = NotificationCompat.PRIORITY_DEFAULT diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/work/TrackerNotificationHelper.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/work/TrackerNotificationHelper.kt index 54bb294a9..6e097ecc6 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/work/TrackerNotificationHelper.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/work/TrackerNotificationHelper.kt @@ -7,7 +7,7 @@ import android.content.Context import android.os.Build import androidx.core.app.NotificationChannelCompat import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationCompat.VISIBILITY_PUBLIC +import androidx.core.app.NotificationCompat.VISIBILITY_PRIVATE import androidx.core.app.NotificationCompat.VISIBILITY_SECRET import androidx.core.app.NotificationManagerCompat import androidx.core.app.PendingIntentCompat @@ -17,12 +17,14 @@ import coil3.request.ImageRequest import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.LocalizedAppContext import org.koitharu.kotatsu.core.model.getLocalizedTitle +import org.koitharu.kotatsu.core.model.isNsfw import org.koitharu.kotatsu.core.nav.AppRouter import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission import org.koitharu.kotatsu.core.util.ext.getQuantityStringSafe import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra import org.koitharu.kotatsu.core.util.ext.toBitmapOrNull +import org.koitharu.kotatsu.parsers.model.ContentRating import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter import javax.inject.Inject @@ -51,7 +53,7 @@ class TrackerNotificationHelper @Inject constructor( if (newChapters.isEmpty() || !applicationContext.checkNotificationPermission(CHANNEL_ID)) { return null } - if (manga.isNsfw && (settings.isTrackerNsfwDisabled || settings.isNsfwContentDisabled)) { + if (manga.isNsfw() && (settings.isTrackerNsfwDisabled || settings.isNsfwContentDisabled)) { return null } val id = manga.url.hashCode() @@ -92,7 +94,7 @@ class TrackerNotificationHelper @Inject constructor( false, ), ) - setVisibility(if (manga.isNsfw) VISIBILITY_SECRET else VISIBILITY_PUBLIC) + setVisibility(if (manga.isNsfw()) VISIBILITY_SECRET else VISIBILITY_PRIVATE) setShortcutId(manga.id.toString()) applyCommonSettings(this) } @@ -127,6 +129,13 @@ class TrackerNotificationHelper @Inject constructor( setNumber(newChaptersCount) setGroup(GROUP_NEW_CHAPTERS) setGroupSummary(true) + setVisibility( + if (notifications.any { it.manga.isNsfw() }) { + VISIBILITY_SECRET + } else { + VISIBILITY_PRIVATE + }, + ) val intent = AppRouter.mangaUpdatesIntent(applicationContext) setContentIntent( PendingIntentCompat.getActivity( From 2bb5673446430cf91372ccd917d15db6b4ebadff Mon Sep 17 00:00:00 2001 From: Draken <131387159+dragonx943@users.noreply.github.com> Date: Fri, 2 May 2025 21:17:22 +0700 Subject: [PATCH 7/9] Update tags_warnlist (cherry picked from commit 8d78b191280e4fb200fe09d26316ee035fa09321) --- app/src/main/res/raw/tags_warnlist | 97 ++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/app/src/main/res/raw/tags_warnlist b/app/src/main/res/raw/tags_warnlist index 62870028c..ec6736948 100644 --- a/app/src/main/res/raw/tags_warnlist +++ b/app/src/main/res/raw/tags_warnlist @@ -20,3 +20,100 @@ scat boys' love girls' love bdsm +futanari +ntr +coprophagia +unbirth +rape +mother +father +sister +shota +shotacon +mother +father +brother +rape +blackmail +lolicon +toddlercon +birth +mind break +ryona +beastiality +urination +slave +human pet +amputee +amputation +gender bender +trans +transgender +full censorship +mosaic +gang rape +furry +inseki +necrophila +prostitution +torture +vore +vaginal birth +parasite +snuff +cannibalism +anal birth +netorase +guro +bestiality +mutilation +vomit +inflation +necrophilia +insect +enema +diapers +beast +parasite +body horror +cbt +piercing +blood +non-consensual +machine +egg laying +femdom +humiliation +public use +bukkake +gangbang +urination +incest +lolicon +drugs +slavery +degradation +bondage +watersports +choking +orgasm denial +beastiality +electrical play +hypno +force +molestation +anal torture +prolapse +electro +knife play +scar +degradation +puke +nipple torture +extreme +violent +degradation +gangbang rape +mindbreak +puppy play +electro play From efff034dc6793ae4a8f6d61c5112d01f990c1cae Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 3 May 2025 08:33:36 +0300 Subject: [PATCH 8/9] Remove duplicated warnlist tags --- app/src/main/res/raw/tags_warnlist | 208 ++++++++++++++--------------- 1 file changed, 98 insertions(+), 110 deletions(-) diff --git a/app/src/main/res/raw/tags_warnlist b/app/src/main/res/raw/tags_warnlist index ec6736948..5f89959f5 100644 --- a/app/src/main/res/raw/tags_warnlist +++ b/app/src/main/res/raw/tags_warnlist @@ -1,119 +1,107 @@ -yaoi -yuri -trap -traps -guro -furry -loli -incest -tentacles -shemale -scat -яой -юри -трап -копро -гуро -тентакли -футанари -инцест -boys' love -girls' love +amputation +amputee +anal birth +anal torture bdsm -futanari -ntr -coprophagia -unbirth -rape -mother +beast +beastiality +bestiality +birth +blackmail +blood +body horror +bondage +boys' love +brother +bukkake +cannibalism +cbt +choking +coprophagia +degradation +diapers +drugs +egg laying +electrical play +electro +electro play +enema +extreme father -sister +femdom +force +full censorship +furry +futanari +gang rape +gangbang +gangbang rape +gender bender +girls' love +guro +human pet +humiliation +hypno +incest +inflation +insect +inseki +knife play +loli +lolicon +machine +mind break +mindbreak +molestation +mosaic +mother +mutilation +necrophila +necrophilia +netorase +nipple torture +non-consensual +ntr +orgasm denial +parasite +piercing +prolapse +prostitution +public use +puke +puppy play +rape +ryona +scar +scat +shemale shota shotacon -mother -father -brother -rape -blackmail -lolicon -toddlercon -birth -mind break -ryona -beastiality -urination +sister slave -human pet -amputee -amputation -gender bender +slavery +snuff +tentacles +toddlercon +torture trans transgender -full censorship -mosaic -gang rape -furry -inseki -necrophila -prostitution -torture -vore -vaginal birth -parasite -snuff -cannibalism -anal birth -netorase -guro -bestiality -mutilation -vomit -inflation -necrophilia -insect -enema -diapers -beast -parasite -body horror -cbt -piercing -blood -non-consensual -machine -egg laying -femdom -humiliation -public use -bukkake -gangbang +trap +traps +unbirth urination -incest -lolicon -drugs -slavery -degradation -bondage -watersports -choking -orgasm denial -beastiality -electrical play -hypno -force -molestation -anal torture -prolapse -electro -knife play -scar -degradation -puke -nipple torture -extreme +vaginal birth violent -degradation -gangbang rape -mindbreak -puppy play -electro play +vomit +vore +watersports +yaoi +yuri +гуро +инцест +копро +тентакли +трап +футанари +юри +яой From 3023c02f12c731e111b3aeb73d999e4fa8a8a7cb Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 3 May 2025 08:37:46 +0300 Subject: [PATCH 9/9] Update parsers --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4c3301bb9..ec0ea14cf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -31,7 +31,7 @@ material = "1.13.0-alpha12" moshi = "1.15.2" okhttp = "4.12.0" okio = "3.11.0" -parsers = "e874837efb" +parsers = "b165a0d611" preference = "1.2.1" recyclerview = "1.4.0" room = "2.6.1"