Compare commits

..

3 Commits

Author SHA1 Message Date
ef9b16da0b Fix favourite categories covers loading 2023-06-02 17:07:26 +03:00
5d1ef983e9 Check available ram before pages prefetch 2023-06-02 17:06:37 +03:00
eb78a776cf Update parsers 2023-06-02 17:04:46 +03:00
9 changed files with 80 additions and 25 deletions

View File

@@ -15,8 +15,8 @@ android {
applicationId 'org.koitharu.kotatsu' applicationId 'org.koitharu.kotatsu'
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 33 targetSdkVersion 33
versionCode 547 versionCode 548
versionName '5.1.3' versionName '5.1.4'
generatedDensities = [] generatedDensities = []
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -78,7 +78,7 @@ afterEvaluate {
} }
dependencies { dependencies {
//noinspection GradleDependency //noinspection GradleDependency
implementation('com.github.KotatsuApp:kotatsu-parsers:fa7ea5b16a') { implementation('com.github.KotatsuApp:kotatsu-parsers:44e28b40d3') {
exclude group: 'org.json', module: 'json' exclude group: 'org.json', module: 'json'
} }
@@ -115,8 +115,8 @@ dependencies {
implementation 'androidx.room:room-ktx:2.5.1' implementation 'androidx.room:room-ktx:2.5.1'
kapt 'androidx.room:room-compiler:2.5.1' kapt 'androidx.room:room-compiler:2.5.1'
implementation 'com.squareup.okhttp3:okhttp:4.10.0' implementation 'com.squareup.okhttp3:okhttp:4.11.0'
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.3' implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.11.0'
implementation 'com.squareup.okio:okio:3.3.0' implementation 'com.squareup.okio:okio:3.3.0'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2' implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2'
@@ -127,8 +127,8 @@ dependencies {
implementation 'androidx.hilt:hilt-work:1.0.0' implementation 'androidx.hilt:hilt-work:1.0.0'
kapt 'androidx.hilt:hilt-compiler:1.0.0' kapt 'androidx.hilt:hilt-compiler:1.0.0'
implementation 'io.coil-kt:coil-base:2.3.0' implementation 'io.coil-kt:coil-base:2.4.0'
implementation 'io.coil-kt:coil-svg:2.3.0' implementation 'io.coil-kt:coil-svg:2.4.0'
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:1b19231b2f' implementation 'com.github.KotatsuApp:subsampling-scale-image-view:1b19231b2f'
implementation 'com.github.solkin:disk-lru-cache:1.4' implementation 'com.github.solkin:disk-lru-cache:1.4'
implementation 'io.noties.markwon:core:4.6.2' implementation 'io.noties.markwon:core:4.6.2'

View File

@@ -6,6 +6,7 @@ import androidx.sqlite.db.SupportSQLiteQuery
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import org.intellij.lang.annotations.Language import org.intellij.lang.annotations.Language
import org.koitharu.kotatsu.core.db.entity.MangaEntity import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.favourites.domain.model.Cover
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.SortOrder
@Dao @Dao
@@ -71,12 +72,12 @@ abstract class FavouritesDao {
) )
abstract suspend fun findAllManga(categoryId: Int): List<MangaEntity> abstract suspend fun findAllManga(categoryId: Int): List<MangaEntity>
suspend fun findCovers(categoryId: Long, order: SortOrder): List<String> { suspend fun findCovers(categoryId: Long, order: SortOrder): List<Cover> {
val orderBy = getOrderBy(order) val orderBy = getOrderBy(order)
@Language("RoomSql") @Language("RoomSql")
val query = SimpleSQLiteQuery( val query = SimpleSQLiteQuery(
"SELECT m.cover_url FROM favourites AS f LEFT JOIN manga AS m ON f.manga_id = m.manga_id " + "SELECT m.cover_url AS url, m.source AS source FROM favourites AS f LEFT JOIN manga AS m ON f.manga_id = m.manga_id " +
"WHERE f.category_id = ? AND deleted_at = 0 ORDER BY $orderBy", "WHERE f.category_id = ? AND deleted_at = 0 ORDER BY $orderBy",
arrayOf<Any>(categoryId), arrayOf<Any>(categoryId),
) )
@@ -145,7 +146,7 @@ abstract class FavouritesDao {
protected abstract fun observeAllImpl(query: SupportSQLiteQuery): Flow<List<FavouriteManga>> protected abstract fun observeAllImpl(query: SupportSQLiteQuery): Flow<List<FavouriteManga>>
@RawQuery @RawQuery
protected abstract suspend fun findCoversImpl(query: SupportSQLiteQuery): List<String> protected abstract suspend fun findCoversImpl(query: SupportSQLiteQuery): List<Cover>
@Query("UPDATE favourites SET deleted_at = :deletedAt WHERE manga_id = :mangaId") @Query("UPDATE favourites SET deleted_at = :deletedAt WHERE manga_id = :mangaId")
protected abstract suspend fun setDeletedAt(mangaId: Long, deletedAt: Long) protected abstract suspend fun setDeletedAt(mangaId: Long, deletedAt: Long)

View File

@@ -18,6 +18,7 @@ import org.koitharu.kotatsu.favourites.data.FavouriteEntity
import org.koitharu.kotatsu.favourites.data.toFavouriteCategory import org.koitharu.kotatsu.favourites.data.toFavouriteCategory
import org.koitharu.kotatsu.favourites.data.toManga import org.koitharu.kotatsu.favourites.data.toManga
import org.koitharu.kotatsu.favourites.data.toMangaList import org.koitharu.kotatsu.favourites.data.toMangaList
import org.koitharu.kotatsu.favourites.domain.model.Cover
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.tracker.work.TrackerNotificationChannels import org.koitharu.kotatsu.tracker.work.TrackerNotificationChannels
@@ -66,11 +67,11 @@ class FavouritesRepository @Inject constructor(
}.distinctUntilChanged() }.distinctUntilChanged()
} }
fun observeCategoriesWithCovers(): Flow<Map<FavouriteCategory, List<String>>> { fun observeCategoriesWithCovers(): Flow<Map<FavouriteCategory, List<Cover>>> {
return db.favouriteCategoriesDao.observeAll() return db.favouriteCategoriesDao.observeAll()
.map { .map {
db.withTransaction { db.withTransaction {
val res = LinkedHashMap<FavouriteCategory, List<String>>() val res = LinkedHashMap<FavouriteCategory, List<Cover>>()
for (entity in it) { for (entity in it) {
val cat = entity.toFavouriteCategory() val cat = entity.toFavouriteCategory()
res[cat] = db.favouritesDao.findCovers( res[cat] = db.favouritesDao.findCovers(

View File

@@ -18,11 +18,12 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.databinding.ItemCategoryBinding import org.koitharu.kotatsu.databinding.ItemCategoryBinding
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesListListener import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesListListener
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.utils.ext.animatorDurationScale
import org.koitharu.kotatsu.utils.ext.disposeImageRequest import org.koitharu.kotatsu.utils.ext.disposeImageRequest
import org.koitharu.kotatsu.utils.ext.enqueueWith import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.getAnimationDuration
import org.koitharu.kotatsu.utils.ext.getThemeColor import org.koitharu.kotatsu.utils.ext.getThemeColor
import org.koitharu.kotatsu.utils.ext.newImageRequest import org.koitharu.kotatsu.utils.ext.newImageRequest
import org.koitharu.kotatsu.utils.ext.source
fun categoryAD( fun categoryAD(
coil: ImageLoader, coil: ImageLoader,
@@ -53,10 +54,7 @@ fun categoryAD(
ColorStateList.valueOf(ColorUtils.setAlphaComponent(backgroundColor, 153)) ColorStateList.valueOf(ColorUtils.setAlphaComponent(backgroundColor, 153))
val fallback = ColorDrawable(Color.TRANSPARENT) val fallback = ColorDrawable(Color.TRANSPARENT)
val coverViews = arrayOf(binding.imageViewCover1, binding.imageViewCover2, binding.imageViewCover3) val coverViews = arrayOf(binding.imageViewCover1, binding.imageViewCover2, binding.imageViewCover3)
val crossFadeDuration = ( val crossFadeDuration = context.getAnimationDuration(R.integer.config_defaultAnimTime).toInt()
context.resources.getInteger(R.integer.config_defaultAnimTime) *
context.animatorDurationScale
).toInt()
itemView.setOnClickListener(eventListener) itemView.setOnClickListener(eventListener)
itemView.setOnLongClickListener(eventListener) itemView.setOnLongClickListener(eventListener)
itemView.setOnTouchListener(eventListener) itemView.setOnTouchListener(eventListener)
@@ -77,9 +75,11 @@ fun categoryAD(
) )
} }
repeat(coverViews.size) { i -> repeat(coverViews.size) { i ->
coverViews[i].newImageRequest(lifecycleOwner, item.covers.getOrNull(i))?.run { val cover = item.covers.getOrNull(i)
coverViews[i].newImageRequest(lifecycleOwner, cover?.url)?.run {
placeholder(R.drawable.ic_placeholder) placeholder(R.drawable.ic_placeholder)
fallback(fallback) fallback(fallback)
source(cover?.mangaSource)
crossfade(crossFadeDuration * (i + 1)) crossfade(crossFadeDuration * (i + 1))
error(R.drawable.ic_error_placeholder) error(R.drawable.ic_error_placeholder)
allowRgb565(true) allowRgb565(true)

View File

@@ -1,11 +1,12 @@
package org.koitharu.kotatsu.favourites.ui.categories.adapter package org.koitharu.kotatsu.favourites.ui.categories.adapter
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.favourites.domain.model.Cover
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
class CategoryListModel( class CategoryListModel(
val mangaCount: Int, val mangaCount: Int,
val covers: List<String>, val covers: List<Cover>,
val category: FavouriteCategory, val category: FavouriteCategory,
val isReorderMode: Boolean, val isReorderMode: Boolean,
) : ListModel { ) : ListModel {
@@ -21,9 +22,7 @@ class CategoryListModel(
if (covers != other.covers) return false if (covers != other.covers) return false
if (category.id != other.category.id) return false if (category.id != other.category.id) return false
if (category.title != other.category.title) return false if (category.title != other.category.title) return false
if (category.order != other.category.order) return false return category.order == other.category.order
return true
} }
override fun hashCode(): Int { override fun hashCode(): Int {
@@ -35,4 +34,4 @@ class CategoryListModel(
result = 31 * result + category.order.hashCode() result = 31 * result + category.order.hashCode()
return result return result
} }
} }

View File

@@ -54,9 +54,10 @@ class PagesCache @Inject constructor(@ApplicationContext context: Context) {
suspend fun put(url: String, source: Source): File = withContext(Dispatchers.IO) { suspend fun put(url: String, source: Source): File = withContext(Dispatchers.IO) {
val file = File(cacheDir.get().parentFile, url.longHashCode().toString()) val file = File(cacheDir.get().parentFile, url.longHashCode().toString())
try { try {
file.sink(append = false).buffer().use { val bytes = file.sink(append = false).buffer().use {
it.writeAllCancellable(source) it.writeAllCancellable(source)
} }
check(bytes != 0L) { "No data has been written" }
lruCache.get().put(url, file) lruCache.get().put(url, file)
} finally { } finally {
file.delete() file.delete()

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.reader.domain package org.koitharu.kotatsu.reader.domain
import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
@@ -8,6 +9,7 @@ import androidx.collection.LongSparseArray
import androidx.collection.set import androidx.collection.set
import dagger.hilt.android.ActivityRetainedLifecycle import dagger.hilt.android.ActivityRetainedLifecycle
import dagger.hilt.android.lifecycle.RetainedLifecycle import dagger.hilt.android.lifecycle.RetainedLifecycle
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.scopes.ActivityRetainedScoped import dagger.hilt.android.scopes.ActivityRetainedScoped
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -30,8 +32,10 @@ import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.await import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import org.koitharu.kotatsu.utils.FileSize
import org.koitharu.kotatsu.utils.RetainedLifecycleCoroutineScope import org.koitharu.kotatsu.utils.RetainedLifecycleCoroutineScope
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.ramAvailable
import org.koitharu.kotatsu.utils.ext.withProgress import org.koitharu.kotatsu.utils.ext.withProgress
import org.koitharu.kotatsu.utils.progress.ProgressDeferred import org.koitharu.kotatsu.utils.progress.ProgressDeferred
import java.io.File import java.io.File
@@ -44,9 +48,11 @@ import kotlin.coroutines.CoroutineContext
private const val PROGRESS_UNDEFINED = -1f private const val PROGRESS_UNDEFINED = -1f
private const val PREFETCH_LIMIT_DEFAULT = 10 private const val PREFETCH_LIMIT_DEFAULT = 10
private const val PREFETCH_MIN_RAM_MB = 80L
@ActivityRetainedScoped @ActivityRetainedScoped
class PageLoader @Inject constructor( class PageLoader @Inject constructor(
@ApplicationContext private val context: Context,
lifecycle: ActivityRetainedLifecycle, lifecycle: ActivityRetainedLifecycle,
private val okHttp: OkHttpClient, private val okHttp: OkHttpClient,
private val cache: PagesCache, private val cache: PagesCache,
@@ -75,7 +81,7 @@ class PageLoader @Inject constructor(
} }
fun isPrefetchApplicable(): Boolean { fun isPrefetchApplicable(): Boolean {
return repository is RemoteMangaRepository && settings.isPagesPreloadEnabled() return repository is RemoteMangaRepository && settings.isPagesPreloadEnabled() && !isLowRam()
} }
@AnyThread @AnyThread
@@ -116,6 +122,9 @@ class PageLoader @Inject constructor(
suspend fun convertInPlace(file: File) { suspend fun convertInPlace(file: File) {
convertLock.withLock { convertLock.withLock {
if (context.ramAvailable < file.length() * 2) {
return@withLock
}
runInterruptible(Dispatchers.Default) { runInterruptible(Dispatchers.Default) {
val image = BitmapFactory.decodeFile(file.absolutePath) val image = BitmapFactory.decodeFile(file.absolutePath)
try { try {
@@ -212,6 +221,10 @@ class PageLoader @Inject constructor(
} }
} }
private fun isLowRam(): Boolean {
return context.ramAvailable <= FileSize.MEGABYTES.convert(PREFETCH_MIN_RAM_MB, FileSize.BYTES)
}
private class InternalErrorHandler : AbstractCoroutineContextElement(CoroutineExceptionHandler), private class InternalErrorHandler : AbstractCoroutineContextElement(CoroutineExceptionHandler),
CoroutineExceptionHandler { CoroutineExceptionHandler {

View File

@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.utils.ext
import android.app.Activity import android.app.Activity
import android.app.ActivityManager import android.app.ActivityManager
import android.app.ActivityManager.MemoryInfo
import android.app.ActivityOptions import android.app.ActivityOptions
import android.content.Context import android.content.Context
import android.content.Context.ACTIVITY_SERVICE import android.content.Context.ACTIVITY_SERVICE
@@ -138,6 +139,13 @@ fun isLowRamDevice(context: Context): Boolean {
return context.activityManager?.isLowRamDevice ?: false return context.activityManager?.isLowRamDevice ?: false
} }
val Context.ramAvailable: Long
get() {
val result = MemoryInfo()
activityManager?.getMemoryInfo(result)
return result.availMem
}
fun scaleUpActivityOptionsOf(view: View): ActivityOptions = ActivityOptions.makeScaleUpAnimation( fun scaleUpActivityOptionsOf(view: View): ActivityOptions = ActivityOptions.makeScaleUpAnimation(
view, view,
0, 0,

View File

@@ -0,0 +1,32 @@
package org.koitharu.kotatsu.favourites.domain.model
import org.koitharu.kotatsu.parsers.model.MangaSource
class Cover(
val url: String,
val source: String,
) {
val mangaSource: MangaSource?
get() = if (source.isEmpty()) null else MangaSource.values().find { it.name == source }
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Cover
if (url != other.url) return false
return source == other.source
}
override fun hashCode(): Int {
var result = url.hashCode()
result = 31 * result + source.hashCode()
return result
}
override fun toString(): String {
return "Cover(url='$url', source=$source)"
}
}