From af209d7048c35f911d4da9bd82e13418750c924e Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 13 Oct 2024 18:24:48 +0300 Subject: [PATCH 1/8] Fix external plugin communication (cherry picked from commit 2214c207425b820ae3d575d7f55f6fb0943df7f0) --- .../core/parser/external/ExternalPluginContentSource.kt | 2 +- .../main/kotlin/org/koitharu/kotatsu/core/util/ext/File.kt | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) 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 103b40d66..bc7280336 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 @@ -141,7 +141,7 @@ class ExternalPluginContentSource( @Blocking @WorkerThread fun getPageUrl(url: String): String { - val uri = "content://${source.authority}/pages/0".toUri().buildUpon() + val uri = "content://${source.authority}/manga/pages/0".toUri().buildUpon() .appendQueryParameter("url", url) .build() return contentResolver.query(uri, null, null, null, null) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/File.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/File.kt index 174b0715c..a653d40c9 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/File.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/File.kt @@ -10,12 +10,11 @@ import android.provider.OpenableColumns import androidx.core.database.getStringOrNull import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runInterruptible -import okhttp3.internal.closeQuietly import org.jetbrains.annotations.Blocking import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.fs.FileSequence +import java.io.BufferedReader import java.io.File -import java.io.InputStream import java.nio.file.attribute.BasicFileAttributes import java.util.zip.ZipEntry import java.util.zip.ZipFile @@ -35,8 +34,8 @@ fun File.takeIfWriteable() = takeIf { it.exists() && it.canWrite() } fun File.isNotEmpty() = length() != 0L @Blocking -fun ZipFile.readText(entry: ZipEntry) = getInputStream(entry).bufferedReader().use { - it.readText() +fun ZipFile.readText(entry: ZipEntry) = getInputStream(entry).use { output -> + output.bufferedReader().use(BufferedReader::readText) } fun File.getStorageName(context: Context): String = runCatching { From 486daf69bffe5cd445024d0925414bf882e1bd72 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 16 Oct 2024 13:19:34 +0300 Subject: [PATCH 2/8] Update link resolver (cherry picked from commit c1d577bdf3388193eae777492006d8772776cbbc) --- app/build.gradle | 2 +- .../kotatsu/core/parser/MangaLinkResolver.kt | 30 +++++++------------ 2 files changed, 11 insertions(+), 21 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 105aa4ac9..01b9251c6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -82,7 +82,7 @@ afterEvaluate { } } dependencies { - implementation('com.github.KotatsuApp:kotatsu-parsers:1.2.2') { + implementation('com.github.KotatsuApp:kotatsu-parsers:d8cb38a9be') { exclude group: 'org.json', module: 'json' } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaLinkResolver.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaLinkResolver.kt index 54bdced1d..60a4f183b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaLinkResolver.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaLinkResolver.kt @@ -7,7 +7,7 @@ import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.UnknownMangaSource import org.koitharu.kotatsu.core.model.isNsfw import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty -import org.koitharu.kotatsu.explore.data.MangaSourcesRepository +import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.exception.NotFoundException import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaListFilter @@ -15,21 +15,20 @@ import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.util.almostEquals import org.koitharu.kotatsu.parsers.util.levenshteinDistance import org.koitharu.kotatsu.parsers.util.runCatchingCancellable -import org.koitharu.kotatsu.parsers.util.toRelativeUrl import javax.inject.Inject @Reusable class MangaLinkResolver @Inject constructor( private val repositoryFactory: MangaRepository.Factory, - private val sourcesRepository: MangaSourcesRepository, private val dataRepository: MangaDataRepository, + private val context: MangaLoaderContext, ) { suspend fun resolve(uri: Uri): Manga { return if (uri.scheme == "kotatsu" || uri.host == "kotatsu.app") { resolveAppLink(uri) } else { - resolveExternalLink(uri) + resolveExternalLink(uri.toString()) } ?: throw NotFoundException("Cannot resolve link", uri.toString()) } @@ -45,18 +44,11 @@ class MangaLinkResolver @Inject constructor( ) } - private suspend fun resolveExternalLink(uri: Uri): Manga? { - dataRepository.findMangaByPublicUrl(uri.toString())?.let { + private suspend fun resolveExternalLink(uri: String): Manga? { + dataRepository.findMangaByPublicUrl(uri)?.let { return it } - val host = uri.host ?: return null - val repo = sourcesRepository.allMangaSources.asSequence() - .map { source -> - repositoryFactory.create(source) as ParserMangaRepository - }.find { repo -> - host in repo.domains - } ?: return null - return repo.findExact(uri.toString().toRelativeUrl(host), null) + return context.newLinkResolver(uri).getManga() } private suspend fun MangaRepository.findExact(url: String?, title: String?): Manga? { @@ -85,12 +77,10 @@ class MangaLinkResolver @Inject constructor( }.getOrThrow() } - private suspend fun MangaRepository.getDetailsNoCache(manga: Manga): Manga { - return if (this is ParserMangaRepository) { - getDetails(manga, CachePolicy.READ_ONLY) - } else { - getDetails(manga) - } + private suspend fun MangaRepository.getDetailsNoCache(manga: Manga): Manga = if (this is CachingMangaRepository) { + getDetails(manga, CachePolicy.READ_ONLY) + } else { + getDetails(manga) } private fun getSeedManga(source: MangaSource, url: String, title: String?) = Manga( From 63054e55d6e768c68b84eafb1725a077073c1ecf Mon Sep 17 00:00:00 2001 From: Marius Albrecht <64852250+MariusAlbrecht@users.noreply.github.com> Date: Tue, 15 Oct 2024 12:39:35 +0200 Subject: [PATCH 3/8] Give "Complete" status only to fully completed Manga Up until now a progress of >= 99.5% would count a Manga as completed (and show the checkmark icon). This causes manga with 200 chapters or more to be marked as completed even if they have at least one unread chapter. https://github.com/KotatsuApp/Kotatsu/issues/1105 (cherry picked from commit b6f57e5656965ed5dd676a1470d9d83985e26ea2) --- .../kotlin/org/koitharu/kotatsu/list/domain/ReadingProgress.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e2c638512..4ce030bf0 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 @@ -38,7 +38,7 @@ data class ReadingProgress( companion object { const val PROGRESS_NONE = -1f - const val PROGRESS_COMPLETED = 0.995f + const val PROGRESS_COMPLETED = 1f fun isValid(percent: Float) = percent in 0f..1f From d59b0626bc146e1ff3c22d211a236e4835096181 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 21 Oct 2024 10:16:54 +0300 Subject: [PATCH 4/8] Fix webtoon page detection #1140 (cherry picked from commit 985b06221809deb78dde6b9581da2b1038e6a6eb) --- .../kotatsu/core/util/ext/RecyclerView.kt | 7 ------- .../ui/pager/webtoon/WebtoonReaderFragment.kt | 17 +++++++++++++---- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/RecyclerView.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/RecyclerView.kt index 0dd4d0cf2..e2170048f 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/RecyclerView.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/RecyclerView.kt @@ -37,13 +37,6 @@ val RecyclerView.visibleItemCount: Int findLastVisibleItemPosition() - findFirstVisibleItemPosition() } ?: 0 -fun RecyclerView.findCenterViewPosition(): Int { - val centerX = width / 2f - val centerY = height / 2f - val view = findChildViewUnder(centerX, centerY) ?: return RecyclerView.NO_POSITION - return getChildAdapterPosition(view) -} - fun RecyclerView.ViewHolder.getItem(clazz: Class): T? { val rawItem = when (this) { is AdapterDelegateViewBindingViewHolder<*, *> -> item diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonReaderFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonReaderFragment.kt index d532aae06..460cc4856 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonReaderFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonReaderFragment.kt @@ -4,6 +4,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.ViewGroup import android.view.animation.DecelerateInterpolator +import androidx.recyclerview.widget.RecyclerView import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.coroutineScope @@ -13,7 +14,6 @@ import kotlinx.coroutines.yield import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.os.NetworkState import org.koitharu.kotatsu.core.ui.list.lifecycle.RecyclerViewLifecycleDispatcher -import org.koitharu.kotatsu.core.util.ext.findCenterViewPosition import org.koitharu.kotatsu.core.util.ext.firstVisibleItemPosition import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.removeItemDecoration @@ -127,14 +127,13 @@ class WebtoonReaderFragment : BaseReaderFragment() } override fun getCurrentState(): ReaderState? = viewBinding?.run { - val currentItem = recyclerView.findCenterViewPosition() + val currentItem = recyclerView.findCurrentPagePosition() val adapter = recyclerView.adapter as? BaseReaderAdapter<*> val page = adapter?.getItemOrNull(currentItem) ?: return@run null ReaderState( chapterId = page.chapterId, page = page.index, - scroll = (recyclerView.findViewHolderForAdapterPosition(currentItem) as? WebtoonHolder) - ?.getScrollY() ?: 0, + scroll = (recyclerView.findViewHolderForAdapterPosition(currentItem) as? WebtoonHolder)?.getScrollY() ?: 0, ) } @@ -168,4 +167,14 @@ class WebtoonReaderFragment : BaseReaderFragment() } return true } + + private fun RecyclerView.findCurrentPagePosition(): Int { + val centerX = width / 2f + val centerY = height - resources.getDimension(R.dimen.webtoon_pages_gap) + if (centerY <= 0) { + return RecyclerView.NO_POSITION + } + val view = findChildViewUnder(centerX, centerY) ?: return RecyclerView.NO_POSITION + return getChildAdapterPosition(view) + } } From 9942ad5e5618c41a1baa9220cf265d8254206479 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 21 Oct 2024 13:34:33 +0300 Subject: [PATCH 5/8] Fix pages loading issues (cherry picked from commit 5bccc595a899285d3e6d770b388908622b082a32) --- app/build.gradle | 2 +- .../util/progress/ProgressResponseBody.kt | 6 ++- .../ui/pager/pages/MangaPageFetcher.kt | 5 +-- .../koitharu/kotatsu/local/data/PagesCache.kt | 40 ++++++++++++++----- .../kotatsu/reader/domain/PageLoader.kt | 13 ++++-- .../reader/ui/pager/PageHolderDelegate.kt | 1 + 6 files changed, 48 insertions(+), 19 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 01b9251c6..5613f92c1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -136,7 +136,7 @@ dependencies { implementation 'io.coil-kt:coil-base:2.7.0' implementation 'io.coil-kt:coil-svg:2.7.0' - implementation 'com.github.KotatsuApp:subsampling-scale-image-view:e04098de68' + implementation 'com.github.KotatsuApp:subsampling-scale-image-view:ac7360c5e3' implementation 'com.github.solkin:disk-lru-cache:1.4' implementation 'io.noties.markwon:core:4.6.2' diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/progress/ProgressResponseBody.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/progress/ProgressResponseBody.kt index b66e5cd2a..06a3fea9b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/progress/ProgressResponseBody.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/progress/ProgressResponseBody.kt @@ -26,8 +26,10 @@ class ProgressResponseBody( override fun contentType(): MediaType? = delegate.contentType() override fun source(): BufferedSource { - return bufferedSource ?: ProgressSource(delegate.source(), contentLength(), progressState).buffer().also { - bufferedSource = it + return bufferedSource ?: synchronized(this) { + bufferedSource ?: ProgressSource(delegate.source(), contentLength(), progressState).buffer().also { + bufferedSource = it + } } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/MangaPageFetcher.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/MangaPageFetcher.kt index a5d8b7082..fd065ee2e 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/MangaPageFetcher.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/MangaPageFetcher.kt @@ -104,10 +104,9 @@ class MangaPageFetcher( if (!response.isSuccessful) { throw HttpException(response) } - val body = response.requireBody() val mimeType = response.mimeType - val file = body.use { - pagesCache.put(pageUrl, it.source()) + val file = response.requireBody().use { + pagesCache.put(pageUrl, it.source(), mimeType) } SourceResult( source = ImageSource( diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/PagesCache.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/PagesCache.kt index a66b60956..dfc86c51d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/PagesCache.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/PagesCache.kt @@ -3,6 +3,7 @@ package org.koitharu.kotatsu.local.data import android.content.Context import android.graphics.Bitmap import android.os.StatFs +import android.webkit.MimeTypeMap import com.tomclaw.cache.DiskLruCache import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers @@ -15,7 +16,7 @@ import okio.use import org.koitharu.kotatsu.core.exceptions.NoDataReceivedException import org.koitharu.kotatsu.core.util.FileSize import org.koitharu.kotatsu.core.util.ext.compressToPNG -import org.koitharu.kotatsu.core.util.ext.longHashCode +import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.subdir import org.koitharu.kotatsu.core.util.ext.takeIfReadable @@ -24,6 +25,7 @@ import org.koitharu.kotatsu.core.util.ext.writeAllCancellable import org.koitharu.kotatsu.parsers.util.SuspendLazy import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import java.io.File +import java.util.UUID import javax.inject.Inject import javax.inject.Singleton @@ -50,15 +52,15 @@ class PagesCache @Inject constructor(@ApplicationContext context: Context) { }.getOrThrow() } - suspend fun get(url: String): File? { + suspend fun get(url: String): File? = withContext(Dispatchers.IO) { val cache = lruCache.get() - return runInterruptible(Dispatchers.IO) { + runInterruptible { cache.get(url)?.takeIfReadable() } } - suspend fun put(url: String, source: Source): File = withContext(Dispatchers.IO) { - val file = File(cacheDir.get().parentFile, url.longHashCode().toString()) + suspend fun put(url: String, source: Source, mimeType: String?): File = withContext(Dispatchers.IO) { + val file = createBufferFile(url, mimeType) try { val bytes = file.sink(append = false).buffer().use { it.writeAllCancellable(source) @@ -66,17 +68,23 @@ class PagesCache @Inject constructor(@ApplicationContext context: Context) { if (bytes == 0L) { throw NoDataReceivedException(url) } - lruCache.get().put(url, file) + val cache = lruCache.get() + runInterruptible { + cache.put(url, file) + } } finally { file.delete() } } suspend fun put(url: String, bitmap: Bitmap): File = withContext(Dispatchers.IO) { - val file = File(cacheDir.get().parentFile, url.longHashCode().toString()) + val file = createBufferFile(url, "image/png") try { bitmap.compressToPNG(file) - lruCache.get().put(url, file) + val cache = lruCache.get() + runInterruptible { + cache.put(url, file) + } } finally { file.delete() } @@ -90,12 +98,24 @@ class PagesCache @Inject constructor(@ApplicationContext context: Context) { } private suspend fun getAvailableSize(): Long = runCatchingCancellable { - val statFs = StatFs(cacheDir.get().absolutePath) - statFs.availableBytes + val dir = cacheDir.get() + runInterruptible(Dispatchers.IO) { + val statFs = StatFs(dir.absolutePath) + statFs.availableBytes + } }.onFailure { it.printStackTraceDebug() }.getOrDefault(SIZE_DEFAULT) + private suspend fun createBufferFile(url: String, mimeType: String?): File { + val ext = mimeType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } + ?: MimeTypeMap.getFileExtensionFromUrl(url).ifNullOrEmpty { "dat" } + val cacheDir = cacheDir.get() + val rootDir = checkNotNull(cacheDir.parentFile) { "Cannot get parent for ${cacheDir.absolutePath}" } + val name = UUID.randomUUID().toString() + "." + ext + return File(rootDir, name) + } + private companion object { val SIZE_MIN diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt index 1b3464c3c..00cbb7ad4 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt @@ -3,8 +3,10 @@ package org.koitharu.kotatsu.reader.domain import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory +import android.graphics.ImageDecoder import android.graphics.Rect import android.net.Uri +import android.os.Build import androidx.annotation.AnyThread import androidx.collection.LongSparseArray import androidx.collection.set @@ -56,6 +58,7 @@ import org.koitharu.kotatsu.local.data.isFileUri import org.koitharu.kotatsu.local.data.isZipUri import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.util.mimeType import org.koitharu.kotatsu.parsers.util.requireBody import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.reader.ui.pager.ReaderPage @@ -149,9 +152,13 @@ class PageLoader @Inject constructor( cache.put(uri.toString(), bitmap).toUri() } else { val file = uri.toFile() - context.ensureRamAtLeast(file.length() * 2) runInterruptible(Dispatchers.IO) { - checkBitmapNotNull(BitmapFactory.decodeFile(file.absolutePath)) + context.ensureRamAtLeast(file.length() * 2) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + ImageDecoder.decodeBitmap(ImageDecoder.createSource(file)) + } else { + checkBitmapNotNull(BitmapFactory.decodeFile(file.absolutePath)) + } }.use { image -> image.compressToPNG(file) } @@ -235,7 +242,7 @@ class PageLoader @Inject constructor( val request = createPageRequest(pageUrl, page.source) imageProxyInterceptor.interceptPageRequest(request, okHttp).ensureSuccess().use { response -> response.requireBody().withProgress(progress).use { - cache.put(pageUrl, it.source()) + cache.put(pageUrl, it.source(), response.mimeType) } }.toUri() } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt index 3cfcea351..7c7ec895f 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt @@ -152,6 +152,7 @@ class PageHolderDelegate( } catch (ce: CancellationException) { throw ce } catch (e2: Throwable) { + e2.printStackTrace() e.addSuppressed(e2) state = State.ERROR callback.onError(e) From 89d395178c0ef3318751896efef387238e92329e Mon Sep 17 00:00:00 2001 From: Koitharu Date: Tue, 22 Oct 2024 13:13:31 +0300 Subject: [PATCH 6/8] Support for AVIF images (cherry picked from commit c15a0ece3ea5287bccef2be4d179cc201d841a54) --- app/build.gradle | 9 +- .../org/koitharu/kotatsu/core/AppModule.kt | 4 + .../kotatsu/core/image/AvifImageDecoder.kt | 67 ++++++++++++ .../kotatsu/core/image/BaseCoilDecoder.kt | 50 +++++++++ .../kotatsu/core/image/BitmapDecoderCompat.kt | 77 +++++++++++++ .../{ui => }/image/RegionBitmapDecoder.kt | 102 +++++++----------- .../koitharu/kotatsu/core/util/ext/Coil.kt | 4 +- .../koitharu/kotatsu/core/util/ext/File.kt | 9 ++ .../org/koitharu/kotatsu/core/util/ext/IO.kt | 10 ++ .../kotatsu/reader/domain/PageLoader.kt | 20 ++-- 10 files changed, 268 insertions(+), 84 deletions(-) create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/core/image/AvifImageDecoder.kt create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/core/image/BaseCoilDecoder.kt create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/core/image/BitmapDecoderCompat.kt rename app/src/main/kotlin/org/koitharu/kotatsu/core/{ui => }/image/RegionBitmapDecoder.kt (73%) diff --git a/app/build.gradle b/app/build.gradle index 5613f92c1..24a2e576e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,8 +16,8 @@ android { applicationId 'org.koitharu.kotatsu' minSdk = 21 targetSdk = 35 - versionCode = 677 - versionName = '7.6.4' + versionCode = 678 + versionName = '7.6.5' generatedDensities = [] testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner' ksp { @@ -92,7 +92,7 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'androidx.core:core-ktx:1.13.1' - implementation 'androidx.activity:activity-ktx:1.9.2' + implementation 'androidx.activity:activity-ktx:1.9.3' implementation 'androidx.fragment:fragment-ktx:1.8.4' implementation 'androidx.transition:transition-ktx:1.5.1' implementation 'androidx.collection:collection-ktx:1.4.4' @@ -136,7 +136,8 @@ dependencies { implementation 'io.coil-kt:coil-base:2.7.0' implementation 'io.coil-kt:coil-svg:2.7.0' - implementation 'com.github.KotatsuApp:subsampling-scale-image-view:ac7360c5e3' + implementation 'org.aomedia.avif.android:avif:1.1.1.14d8e3c4' + implementation 'com.github.KotatsuApp:subsampling-scale-image-view:d1d10a6975' implementation 'com.github.solkin:disk-lru-cache:1.4' implementation 'io.noties.markwon:core:4.6.2' diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/AppModule.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/AppModule.kt index cf47f00b2..cd1a77f51 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/AppModule.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/AppModule.kt @@ -27,6 +27,8 @@ import okhttp3.OkHttpClient import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.browser.cloudflare.CaptchaNotifier import org.koitharu.kotatsu.core.db.MangaDatabase +import org.koitharu.kotatsu.core.image.AvifImageDecoder +import org.koitharu.kotatsu.core.image.RegionBitmapDecoder import org.koitharu.kotatsu.core.network.MangaHttpClient import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor import org.koitharu.kotatsu.core.os.AppShortcutManager @@ -119,6 +121,8 @@ interface AppModule { ComponentRegistry.Builder() .add(SvgDecoder.Factory()) .add(CbzFetcher.Factory()) + .add(AvifImageDecoder.Factory()) + .add(RegionBitmapDecoder.Factory()) .add(FaviconFetcher.Factory(context, okHttpClientLazy, mangaRepositoryFactory)) .add(MangaPageKeyer()) .add(pageFetcherFactory) 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 new file mode 100644 index 000000000..9671baa73 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/image/AvifImageDecoder.kt @@ -0,0 +1,67 @@ +package org.koitharu.kotatsu.core.image + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import androidx.core.graphics.drawable.toDrawable +import coil.ImageLoader +import coil.decode.DecodeResult +import coil.decode.Decoder +import coil.decode.ImageSource +import coil.fetch.SourceResult +import coil.request.Options +import com.davemorrissey.labs.subscaleview.decoder.ImageDecodeException +import kotlinx.coroutines.sync.Semaphore +import org.aomedia.avif.android.AvifDecoder +import org.aomedia.avif.android.AvifDecoder.Info +import org.koitharu.kotatsu.core.util.ext.toByteBuffer + +class AvifImageDecoder(source: ImageSource, options: Options, parallelismLock: Semaphore) : + BaseCoilDecoder(source, options, parallelismLock) { + + override fun BitmapFactory.Options.decode(): DecodeResult { + val bytes = source.source().use { + it.inputStream().toByteBuffer() + } + 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 = Bitmap.createBitmap(info.width, info.height, config) + if (!AvifDecoder.decode(bytes, bytes.remaining(), bitmap)) { + bitmap.recycle() + throw ImageDecodeException(null, "avif") + } + return DecodeResult( + drawable = bitmap.toDrawable(options.context.resources), + isSampled = false, + ) + } + + class Factory : Decoder.Factory { + + private val parallelismLock = Semaphore(DEFAULT_PARALLELISM) + + override fun create( + result: SourceResult, + options: Options, + imageLoader: ImageLoader + ): Decoder? = if (isApplicable(result)) { + AvifImageDecoder(result.source, options, parallelismLock) + } else { + null + } + + override fun equals(other: Any?) = other is Factory + + override fun hashCode() = javaClass.hashCode() + + private fun isApplicable(result: SourceResult): Boolean { + return result.mimeType == "image/avif" + } + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/image/BaseCoilDecoder.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/image/BaseCoilDecoder.kt new file mode 100644 index 000000000..fc621ad49 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/image/BaseCoilDecoder.kt @@ -0,0 +1,50 @@ +package org.koitharu.kotatsu.core.image + +import android.graphics.BitmapFactory +import coil.decode.DecodeResult +import coil.decode.Decoder +import coil.decode.ImageSource +import coil.request.Options +import coil.size.Dimension +import coil.size.Scale +import coil.size.Size +import coil.size.isOriginal +import coil.size.pxOrElse +import kotlinx.coroutines.runInterruptible +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withPermit +import org.jetbrains.annotations.Blocking + +abstract class BaseCoilDecoder( + protected val source: ImageSource, + protected val options: Options, + private val parallelismLock: Semaphore, +) : Decoder { + + final override suspend fun decode(): DecodeResult = parallelismLock.withPermit { + runInterruptible { BitmapFactory.Options().decode() } + } + + @Blocking + protected abstract fun BitmapFactory.Options.decode(): DecodeResult + + protected companion object { + + const val DEFAULT_PARALLELISM = 4 + + inline fun Size.widthPx(scale: Scale, original: () -> Int): Int { + return if (isOriginal) original() else width.toPx(scale) + } + + inline fun Size.heightPx(scale: Scale, original: () -> Int): Int { + return if (isOriginal) original() else height.toPx(scale) + } + + fun Dimension.toPx(scale: Scale) = pxOrElse { + when (scale) { + Scale.FILL -> Int.MIN_VALUE + Scale.FIT -> Int.MAX_VALUE + } + } + } +} 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 new file mode 100644 index 000000000..80e9a4bdb --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/image/BitmapDecoderCompat.kt @@ -0,0 +1,77 @@ +package org.koitharu.kotatsu.core.image + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.ImageDecoder +import android.os.Build +import android.webkit.MimeTypeMap +import com.davemorrissey.labs.subscaleview.decoder.ImageDecodeException +import okhttp3.MediaType +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import org.aomedia.avif.android.AvifDecoder +import org.aomedia.avif.android.AvifDecoder.Info +import org.jetbrains.annotations.Blocking +import org.koitharu.kotatsu.core.util.ext.toByteBuffer +import java.io.File +import java.io.InputStream +import java.nio.ByteBuffer +import java.nio.file.Files + +object BitmapDecoderCompat { + + private const val FORMAT_AVIF = "avif" + + @Blocking + fun decode(file: File): Bitmap = when (val format = getMimeType(file)?.subtype) { + FORMAT_AVIF -> file.inputStream().use { decodeAvif(it.toByteBuffer()) } + else -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + ImageDecoder.decodeBitmap(ImageDecoder.createSource(file)) + } else { + checkBitmapNotNull(BitmapFactory.decodeFile(file.absolutePath), format) + } + } + + @Blocking + fun decode(stream: InputStream, type: MediaType?): Bitmap { + val format = type?.subtype + if (format == FORMAT_AVIF) { + return decodeAvif(stream.toByteBuffer()) + } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { + return checkBitmapNotNull(BitmapFactory.decodeStream(stream), format) + } + val byteBuffer = stream.toByteBuffer() + return if (AvifDecoder.isAvifImage(byteBuffer)) { + decodeAvif(byteBuffer) + } else { + ImageDecoder.decodeBitmap(ImageDecoder.createSource(byteBuffer)) + } + } + + private fun getMimeType(file: File): MediaType? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + Files.probeContentType(file.toPath())?.toMediaTypeOrNull() + } else { + MimeTypeMap.getSingleton().getMimeTypeFromExtension(file.extension)?.toMediaTypeOrNull() + } + + private fun checkBitmapNotNull(bitmap: Bitmap?, format: String?): Bitmap = + bitmap ?: throw ImageDecodeException(null, format) + + private fun decodeAvif(bytes: ByteBuffer): Bitmap { + val info = Info() + if (!AvifDecoder.getInfo(bytes, bytes.remaining(), info)) { + throw ImageDecodeException( + null, + FORMAT_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 = Bitmap.createBitmap(info.width, info.height, config) + if (!AvifDecoder.decode(bytes, bytes.remaining(), bitmap)) { + bitmap.recycle() + throw ImageDecodeException(null, FORMAT_AVIF) + } + return bitmap + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/RegionBitmapDecoder.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/image/RegionBitmapDecoder.kt similarity index 73% rename from app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/RegionBitmapDecoder.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/image/RegionBitmapDecoder.kt index 47d5461cb..d55c145a0 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/RegionBitmapDecoder.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/image/RegionBitmapDecoder.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.core.ui.image +package org.koitharu.kotatsu.core.image import android.graphics.Bitmap import android.graphics.BitmapFactory @@ -13,27 +13,14 @@ import coil.decode.Decoder import coil.decode.ImageSource import coil.fetch.SourceResult import coil.request.Options -import coil.size.Dimension -import coil.size.Scale -import coil.size.Size -import coil.size.isOriginal -import coil.size.pxOrElse -import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.sync.Semaphore -import kotlinx.coroutines.sync.withPermit import kotlin.math.roundToInt class RegionBitmapDecoder( - private val source: ImageSource, - private val options: Options, - private val parallelismLock: Semaphore, -) : Decoder { + source: ImageSource, options: Options, parallelismLock: Semaphore +) : BaseCoilDecoder(source, options, parallelismLock) { - override suspend fun decode() = parallelismLock.withPermit { - runInterruptible { BitmapFactory.Options().decode() } - } - - private fun BitmapFactory.Options.decode(): DecodeResult { + override fun BitmapFactory.Options.decode(): DecodeResult { val regionDecoder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { BitmapRegionDecoder.newInstance(source.source().inputStream()) } else { @@ -55,29 +42,6 @@ class RegionBitmapDecoder( } } - private fun BitmapFactory.Options.configureConfig() { - var config = options.config - - inMutable = false - - if (Build.VERSION.SDK_INT >= 26 && options.colorSpace != null) { - inPreferredColorSpace = options.colorSpace - } - inPremultiplied = options.premultipliedAlpha - - // Decode the image as RGB_565 as an optimization if allowed. - if (options.allowRgb565 && config == Bitmap.Config.ARGB_8888 && outMimeType == "image/jpeg") { - config = Bitmap.Config.RGB_565 - } - - // High color depth images must be decoded as either RGBA_F16 or HARDWARE. - if (Build.VERSION.SDK_INT >= 26 && outConfig == Bitmap.Config.RGBA_F16 && config != Bitmap.Config.HARDWARE) { - config = Bitmap.Config.RGBA_F16 - } - - inPreferredConfig = config - } - /** Compute and set the scaling properties for [BitmapFactory.Options]. */ private fun BitmapFactory.Options.configureScale(srcWidth: Int, srcHeight: Int): Rect { val dstWidth = options.size.widthPx(options.scale) { srcWidth } @@ -142,18 +106,41 @@ class RegionBitmapDecoder( return rect } - class Factory( - maxParallelism: Int = DEFAULT_MAX_PARALLELISM, - ) : Decoder.Factory { + private fun BitmapFactory.Options.configureConfig() { + var config = options.config - @Suppress("NEWER_VERSION_IN_SINCE_KOTLIN") - @SinceKotlin("999.9") // Only public in Java. - constructor() : this() + inMutable = false - private val parallelismLock = Semaphore(maxParallelism) + if (Build.VERSION.SDK_INT >= 26 && options.colorSpace != null) { + inPreferredColorSpace = options.colorSpace + } + inPremultiplied = options.premultipliedAlpha + + // Decode the image as RGB_565 as an optimization if allowed. + if (options.allowRgb565 && config == Bitmap.Config.ARGB_8888 && outMimeType == "image/jpeg") { + config = Bitmap.Config.RGB_565 + } - override fun create(result: SourceResult, options: Options, imageLoader: ImageLoader): Decoder { - return RegionBitmapDecoder(result.source, options, parallelismLock) + // High color depth images must be decoded as either RGBA_F16 or HARDWARE. + if (Build.VERSION.SDK_INT >= 26 && outConfig == Bitmap.Config.RGBA_F16 && config != Bitmap.Config.HARDWARE) { + config = Bitmap.Config.RGBA_F16 + } + + inPreferredConfig = config + } + + class Factory : Decoder.Factory { + + private val parallelismLock = Semaphore(DEFAULT_PARALLELISM) + + override fun create( + result: SourceResult, + options: Options, + imageLoader: ImageLoader + ): Decoder? = if (options.parameters.value(PARAM_REGION) == true) { + RegionBitmapDecoder(result.source, options, parallelismLock) + } else { + null } override fun equals(other: Any?) = other is Factory @@ -164,22 +151,7 @@ class RegionBitmapDecoder( companion object { const val PARAM_SCROLL = "scroll" + const val PARAM_REGION = "region" const val SCROLL_UNDEFINED = -1 - private const val DEFAULT_MAX_PARALLELISM = 4 - - private inline fun Size.widthPx(scale: Scale, original: () -> Int): Int { - return if (isOriginal) original() else width.toPx(scale) - } - - private inline fun Size.heightPx(scale: Scale, original: () -> Int): Int { - return if (isOriginal) original() else height.toPx(scale) - } - - private fun Dimension.toPx(scale: Scale) = pxOrElse { - when (scale) { - Scale.FILL -> Int.MIN_VALUE - Scale.FIT -> Int.MAX_VALUE - } - } } } 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 580b5aeed..19ffb7481 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 @@ -14,8 +14,8 @@ import coil.request.SuccessResult import coil.util.CoilUtils import com.google.android.material.progressindicator.BaseProgressIndicator import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.image.RegionBitmapDecoder import org.koitharu.kotatsu.core.ui.image.AnimatedPlaceholderDrawable -import org.koitharu.kotatsu.core.ui.image.RegionBitmapDecoder import org.koitharu.kotatsu.core.util.progress.ImageRequestIndicatorListener import org.koitharu.kotatsu.parsers.model.MangaSource import com.google.android.material.R as materialR @@ -63,7 +63,7 @@ fun ImageRequest.Builder.indicator(indicators: List>): fun ImageRequest.Builder.decodeRegion( scroll: Int = RegionBitmapDecoder.SCROLL_UNDEFINED, -): ImageRequest.Builder = decoderFactory(RegionBitmapDecoder.Factory()) +): ImageRequest.Builder = setParameter(RegionBitmapDecoder.PARAM_REGION, true) .setParameter(RegionBitmapDecoder.PARAM_SCROLL, scroll) @Suppress("SpellCheckingInspection") diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/File.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/File.kt index a653d40c9..ca7840a9b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/File.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/File.kt @@ -7,9 +7,12 @@ import android.os.Build import android.os.Environment import android.os.storage.StorageManager import android.provider.OpenableColumns +import android.webkit.MimeTypeMap import androidx.core.database.getStringOrNull import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runInterruptible +import okhttp3.MediaType +import okhttp3.MediaType.Companion.toMediaTypeOrNull import org.jetbrains.annotations.Blocking import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.fs.FileSequence @@ -38,6 +41,12 @@ fun ZipFile.readText(entry: ZipEntry) = getInputStream(entry).use { output -> output.bufferedReader().use(BufferedReader::readText) } +val ZipEntry.mimeType: MediaType? + get() { + val ext = name.substringAfterLast('.') + return MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext)?.toMediaTypeOrNull() + } + fun File.getStorageName(context: Context): String = runCatching { val manager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 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 d41e0ba38..41cf24f06 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 @@ -10,6 +10,9 @@ import okio.BufferedSink import okio.Source import org.koitharu.kotatsu.core.util.CancellableSource import org.koitharu.kotatsu.core.util.progress.ProgressResponseBody +import java.io.ByteArrayOutputStream +import java.io.InputStream +import java.nio.ByteBuffer fun ResponseBody.withProgress(progressState: MutableStateFlow): ResponseBody { return ProgressResponseBody(this, progressState) @@ -23,3 +26,10 @@ suspend fun Source.cancellable(): Source { suspend fun BufferedSink.writeAllCancellable(source: Source) = withContext(Dispatchers.IO) { writeAll(source.cancellable()) } + +fun InputStream.toByteBuffer(): ByteBuffer { + val outStream = ByteArrayOutputStream(available()) + copyTo(outStream) + val bytes = outStream.toByteArray() + return ByteBuffer.allocateDirect(bytes.size).put(bytes).position(0) as ByteBuffer +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt index 00cbb7ad4..8406ff161 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt @@ -1,12 +1,10 @@ package org.koitharu.kotatsu.reader.domain +import android.content.ContentResolver.MimeTypeInfo import android.content.Context -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.graphics.ImageDecoder import android.graphics.Rect import android.net.Uri -import android.os.Build +import android.webkit.MimeTypeMap import androidx.annotation.AnyThread import androidx.collection.LongSparseArray import androidx.collection.set @@ -61,6 +59,8 @@ import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.util.mimeType import org.koitharu.kotatsu.parsers.util.requireBody import org.koitharu.kotatsu.parsers.util.runCatchingCancellable +import org.koitharu.kotatsu.core.image.BitmapDecoderCompat +import org.koitharu.kotatsu.core.util.ext.mimeType import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import java.util.LinkedList import java.util.concurrent.atomic.AtomicInteger @@ -144,8 +144,8 @@ class PageLoader @Inject constructor( ZipFile(uri.schemeSpecificPart).use { zip -> val entry = zip.getEntry(uri.fragment) context.ensureRamAtLeast(entry.size * 2) - zip.getInputStream(zip.getEntry(uri.fragment)).use { - checkBitmapNotNull(BitmapFactory.decodeStream(it)) + zip.getInputStream(entry).use { + BitmapDecoderCompat.decode(it, entry.mimeType) } } } @@ -154,11 +154,7 @@ class PageLoader @Inject constructor( val file = uri.toFile() runInterruptible(Dispatchers.IO) { context.ensureRamAtLeast(file.length() * 2) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - ImageDecoder.decodeBitmap(ImageDecoder.createSource(file)) - } else { - checkBitmapNotNull(BitmapFactory.decodeFile(file.absolutePath)) - } + BitmapDecoderCompat.decode(file) }.use { image -> image.compressToPNG(file) } @@ -253,8 +249,6 @@ class PageLoader @Inject constructor( return context.ramAvailable <= FileSize.MEGABYTES.convert(PREFETCH_MIN_RAM_MB, FileSize.BYTES) } - private fun checkBitmapNotNull(bitmap: Bitmap?): Bitmap = checkNotNull(bitmap) { "Cannot decode bitmap" } - private fun Deferred.isValid(): Boolean { return getCompletionResultOrNull()?.map { uri -> uri.exists() && uri.isTargetNotEmpty() From 4855b2c1608dd3d287fea403ffeb5931551d97b0 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 23 Oct 2024 09:10:43 +0300 Subject: [PATCH 7/8] Fix RegionBitmapDecode usage --- .../main/kotlin/org/koitharu/kotatsu/core/AppModule.kt | 6 +----- .../koitharu/kotatsu/core/image/RegionBitmapDecoder.kt | 9 ++------- .../kotlin/org/koitharu/kotatsu/core/util/ext/Coil.kt | 2 +- 3 files changed, 4 insertions(+), 13 deletions(-) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/AppModule.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/AppModule.kt index cd1a77f51..c0133b6e8 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/AppModule.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/AppModule.kt @@ -28,7 +28,6 @@ import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.browser.cloudflare.CaptchaNotifier import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.image.AvifImageDecoder -import org.koitharu.kotatsu.core.image.RegionBitmapDecoder import org.koitharu.kotatsu.core.network.MangaHttpClient import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor import org.koitharu.kotatsu.core.os.AppShortcutManager @@ -82,9 +81,7 @@ interface AppModule { @Singleton fun provideMangaDatabase( @ApplicationContext context: Context, - ): MangaDatabase { - return MangaDatabase(context) - } + ): MangaDatabase = MangaDatabase(context) @Provides @Singleton @@ -122,7 +119,6 @@ interface AppModule { .add(SvgDecoder.Factory()) .add(CbzFetcher.Factory()) .add(AvifImageDecoder.Factory()) - .add(RegionBitmapDecoder.Factory()) .add(FaviconFetcher.Factory(context, okHttpClientLazy, mangaRepositoryFactory)) .add(MangaPageKeyer()) .add(pageFetcherFactory) 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 d55c145a0..bff3abfba 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 @@ -129,7 +129,7 @@ class RegionBitmapDecoder( inPreferredConfig = config } - class Factory : Decoder.Factory { + object Factory : Decoder.Factory { private val parallelismLock = Semaphore(DEFAULT_PARALLELISM) @@ -137,11 +137,7 @@ class RegionBitmapDecoder( result: SourceResult, options: Options, imageLoader: ImageLoader - ): Decoder? = if (options.parameters.value(PARAM_REGION) == true) { - RegionBitmapDecoder(result.source, options, parallelismLock) - } else { - null - } + ): Decoder = RegionBitmapDecoder(result.source, options, parallelismLock) override fun equals(other: Any?) = other is Factory @@ -151,7 +147,6 @@ class RegionBitmapDecoder( companion object { const val PARAM_SCROLL = "scroll" - const val PARAM_REGION = "region" const val SCROLL_UNDEFINED = -1 } } 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 19ffb7481..c50b0daf0 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 @@ -63,7 +63,7 @@ fun ImageRequest.Builder.indicator(indicators: List>): fun ImageRequest.Builder.decodeRegion( scroll: Int = RegionBitmapDecoder.SCROLL_UNDEFINED, -): ImageRequest.Builder = setParameter(RegionBitmapDecoder.PARAM_REGION, true) +): ImageRequest.Builder = decoderFactory(RegionBitmapDecoder.Factory) .setParameter(RegionBitmapDecoder.PARAM_SCROLL, scroll) @Suppress("SpellCheckingInspection") From 855b55da9d3c044f8f6c40df4762ca1381d7cc66 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 23 Oct 2024 19:47:27 +0300 Subject: [PATCH 8/8] Update parsers --- app/build.gradle | 2 +- .../kotatsu/core/backup/BackupRepository.kt | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 24a2e576e..afbbe062b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -82,7 +82,7 @@ afterEvaluate { } } dependencies { - implementation('com.github.KotatsuApp:kotatsu-parsers:d8cb38a9be') { + implementation('com.github.KotatsuApp:kotatsu-parsers:3d5cc5ceff') { exclude group: 'org.json', module: 'json' } 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 2071ae6d9..a8c45f0ca 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 @@ -6,7 +6,7 @@ import org.json.JSONObject import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.parsers.util.json.JSONIterator +import org.koitharu.kotatsu.parsers.util.json.asTypedList import org.koitharu.kotatsu.parsers.util.json.getLongOrDefault import org.koitharu.kotatsu.parsers.util.json.mapJSON import org.koitharu.kotatsu.parsers.util.runCatchingCancellable @@ -130,7 +130,7 @@ class BackupRepository @Inject constructor( suspend fun restoreHistory(entry: BackupEntry): CompositeResult { val result = CompositeResult() - for (item in entry.data.JSONIterator()) { + for (item in entry.data.asTypedList()) { val mangaJson = item.getJSONObject("manga") val manga = JsonDeserializer(mangaJson).toMangaEntity() val tags = mangaJson.getJSONArray("tags").mapJSON { @@ -150,7 +150,7 @@ class BackupRepository @Inject constructor( suspend fun restoreCategories(entry: BackupEntry): CompositeResult { val result = CompositeResult() - for (item in entry.data.JSONIterator()) { + for (item in entry.data.asTypedList()) { val category = JsonDeserializer(item).toFavouriteCategoryEntity() result += runCatchingCancellable { db.getFavouriteCategoriesDao().upsert(category) @@ -161,7 +161,7 @@ class BackupRepository @Inject constructor( suspend fun restoreFavourites(entry: BackupEntry): CompositeResult { val result = CompositeResult() - for (item in entry.data.JSONIterator()) { + for (item in entry.data.asTypedList()) { val mangaJson = item.getJSONObject("manga") val manga = JsonDeserializer(mangaJson).toMangaEntity() val tags = mangaJson.getJSONArray("tags").mapJSON { @@ -181,7 +181,7 @@ class BackupRepository @Inject constructor( suspend fun restoreBookmarks(entry: BackupEntry): CompositeResult { val result = CompositeResult() - for (item in entry.data.JSONIterator()) { + for (item in entry.data.asTypedList()) { val mangaJson = item.getJSONObject("manga") val manga = JsonDeserializer(mangaJson).toMangaEntity() val tags = item.getJSONArray("tags").mapJSON { @@ -203,7 +203,7 @@ class BackupRepository @Inject constructor( suspend fun restoreSources(entry: BackupEntry): CompositeResult { val result = CompositeResult() - for (item in entry.data.JSONIterator()) { + for (item in entry.data.asTypedList()) { val source = JsonDeserializer(item).toMangaSourceEntity() result += runCatchingCancellable { db.getSourcesDao().upsert(source) @@ -214,7 +214,7 @@ class BackupRepository @Inject constructor( fun restoreSettings(entry: BackupEntry): CompositeResult { val result = CompositeResult() - for (item in entry.data.JSONIterator()) { + for (item in entry.data.asTypedList()) { result += runCatchingCancellable { settings.upsertAll(JsonDeserializer(item).toMap()) }