Block AD in Large preview (#1694)
* Cleanup with `getOrPut` * Refine `ArchivePageLoader` naming * Move `PageLoader` * Bump ktlint to fix when guards * Extract `AdsPlaceholder` * Detect QrCode for LargePreview * Conditional detect * Fixes * Fixes * Cleanup code * Fix render * Fixup hardware bitmap * Original size to detect * Log QrCode detect exception * Don't mutate cached * Refine ListUrlBuilder * Move functions * Enable preview adblocker for debug build only --------- Co-authored-by: Dude so hot <djohn@fbi.gov>
This commit is contained in:
1 parent
9ead29643d
commit
04ee1a6362
21 files changed
+192
-150
No files matched your search
@@ -23,6 +23,7 @@ import androidx.room.PrimaryKey
|
||||
import androidx.room.TypeConverter
|
||||
import androidx.room.TypeConverters
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.NOT_FAVORITED
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@@ -90,6 +91,9 @@ data class BaseGalleryInfo(
|
||||
override var favoriteNote: String? = null,
|
||||
) : GalleryInfo,
|
||||
Parcelable {
|
||||
@IgnoredOnParcel
|
||||
override val hasAds by lazy { simpleTags?.any { "extraneous ads" in it } == true }
|
||||
|
||||
constructor() : this(0, "")
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ interface GalleryInfo {
|
||||
var favoriteSlot: Int
|
||||
var favoriteName: String?
|
||||
var favoriteNote: String?
|
||||
val hasAds: Boolean
|
||||
|
||||
fun generateSLang() {
|
||||
simpleLanguage = simpleTags?.let { generateSLangFromTags(it) }
|
||||
@@ -141,6 +142,3 @@ fun GalleryInfo.findBaseInfo(): BaseGalleryInfo = when (this) {
|
||||
}
|
||||
|
||||
fun GalleryInfo.asGalleryDetail(): GalleryDetail? = this as? GalleryDetail
|
||||
|
||||
val GalleryInfo.hasAds
|
||||
get() = simpleTags?.any { "extraneous ads" in it } ?: false
|
||||
@@ -18,7 +18,7 @@ package com.hippo.ehviewer.client.data
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
interface GalleryPreview : Parcelable {
|
||||
sealed interface GalleryPreview : Parcelable {
|
||||
val url: String
|
||||
val position: Int
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
package com.hippo.ehviewer.client.data
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.IntDef
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
@@ -32,13 +31,12 @@ import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class ListUrlBuilder(
|
||||
@get:Mode @Mode
|
||||
var mode: Int = MODE_NORMAL,
|
||||
private var mPrev: String? = null,
|
||||
var mNext: String? = null,
|
||||
private var prev: String? = null,
|
||||
var next: String? = null,
|
||||
// Reset to null after initial loading
|
||||
var mJumpTo: String? = null,
|
||||
private var mRange: Int = 0,
|
||||
var jumpTo: String? = null,
|
||||
private var range: Int = 0,
|
||||
var category: Int = EhUtils.NONE,
|
||||
private var mKeyword: String? = null,
|
||||
var hash: String? = null,
|
||||
@@ -47,24 +45,23 @@ data class ListUrlBuilder(
|
||||
var minRating: Int = -1,
|
||||
var pageFrom: Int = -1,
|
||||
var pageTo: Int = -1,
|
||||
var imagePath: String? = null,
|
||||
) : Parcelable {
|
||||
|
||||
fun setIndex(index: String, isNext: Boolean = true) {
|
||||
mNext = index.takeIf { isNext }
|
||||
mPrev = index.takeUnless { isNext }
|
||||
mRange = 0
|
||||
next = index.takeIf { isNext }
|
||||
prev = index.takeUnless { isNext }
|
||||
range = 0
|
||||
}
|
||||
|
||||
fun setJumpTo(jumpTo: Int) {
|
||||
mJumpTo = jumpTo.takeUnless { it == 0 }?.toString()
|
||||
fun setJumpTo(to: Int) {
|
||||
jumpTo = to.takeUnless { it == 0 }?.toString()
|
||||
}
|
||||
|
||||
fun setRange(range: Int) {
|
||||
mRange = range
|
||||
mPrev = null
|
||||
mNext = null
|
||||
mJumpTo = null
|
||||
fun setRange(to: Int) {
|
||||
range = to
|
||||
prev = null
|
||||
next = null
|
||||
jumpTo = null
|
||||
}
|
||||
|
||||
var keyword: String?
|
||||
@@ -81,7 +78,7 @@ data class ListUrlBuilder(
|
||||
minRating = q.minRating
|
||||
pageFrom = q.pageFrom
|
||||
pageTo = q.pageTo
|
||||
mNext = q.name.substringAfterLast('@', "").ifEmpty { null }
|
||||
next = q.name.substringAfterLast('@', "").ifEmpty { null }
|
||||
}
|
||||
|
||||
fun toQuickSearch(name: String): QuickSearch = QuickSearch(
|
||||
@@ -277,10 +274,10 @@ data class ListUrlBuilder(
|
||||
}
|
||||
}
|
||||
addQueryParameterIfNotBlank("f_search", query)
|
||||
addQueryParameterIfNotBlank("prev", mPrev)
|
||||
addQueryParameterIfNotBlank("next", mNext)
|
||||
addQueryParameterIfNotBlank("seek", mJumpTo)
|
||||
addQueryParameterIfNotBlank("range", mRange.takeIf { it > 0 }?.toString())
|
||||
addQueryParameterIfNotBlank("prev", prev)
|
||||
addQueryParameterIfNotBlank("next", next)
|
||||
addQueryParameterIfNotBlank("seek", jumpTo)
|
||||
addQueryParameterIfNotBlank("range", range.takeIf { it > 0 }?.toString())
|
||||
// Advance search
|
||||
if (advanceSearch > 0 || minRating > 0 || pageFrom > 0 || pageTo > 0) {
|
||||
addQueryParameter("advsearch", "1")
|
||||
@@ -322,10 +319,10 @@ data class ListUrlBuilder(
|
||||
MODE_UPLOADER, MODE_TAG -> {
|
||||
val path = if (mode == MODE_UPLOADER) "uploader" else "tag"
|
||||
ehUrl(listOf(path, requireNotNull(mKeyword))) {
|
||||
addQueryParameterIfNotBlank("prev", mPrev)
|
||||
addQueryParameterIfNotBlank("next", mNext)
|
||||
addQueryParameterIfNotBlank("seek", mJumpTo)
|
||||
addQueryParameterIfNotBlank("range", mRange.takeIf { it > 0 }?.toString())
|
||||
addQueryParameterIfNotBlank("prev", prev)
|
||||
addQueryParameterIfNotBlank("next", next)
|
||||
addQueryParameterIfNotBlank("seek", jumpTo)
|
||||
addQueryParameterIfNotBlank("range", range.takeIf { it > 0 }?.toString())
|
||||
}.buildString()
|
||||
}
|
||||
|
||||
@@ -333,27 +330,14 @@ data class ListUrlBuilder(
|
||||
MODE_IMAGE_SEARCH -> ehUrl {
|
||||
addQueryParameter("f_shash", requireNotNull(hash))
|
||||
}.buildString()
|
||||
MODE_TOPLIST -> {
|
||||
ehUrl("toplist.php", EhUrl.DOMAIN_E) {
|
||||
addQueryParameter("tl", requireNotNull(mKeyword))
|
||||
addQueryParameterIfNotBlank("p", mJumpTo)
|
||||
}.buildString()
|
||||
}
|
||||
MODE_TOPLIST -> ehUrl("toplist.php", EhUrl.DOMAIN_E) {
|
||||
addQueryParameter("tl", requireNotNull(mKeyword))
|
||||
addQueryParameterIfNotBlank("p", jumpTo)
|
||||
}.buildString()
|
||||
|
||||
else -> throw IllegalStateException("Unexpected value: $mode")
|
||||
}
|
||||
|
||||
@IntDef(
|
||||
MODE_NORMAL,
|
||||
MODE_UPLOADER,
|
||||
MODE_TAG,
|
||||
MODE_WHATS_HOT,
|
||||
MODE_IMAGE_SEARCH,
|
||||
MODE_SUBSCRIPTION,
|
||||
MODE_TOPLIST,
|
||||
)
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
private annotation class Mode
|
||||
companion object {
|
||||
const val MODE_NORMAL = 0x0
|
||||
const val MODE_UPLOADER = 0x1
|
||||
|
||||
@@ -9,12 +9,16 @@ import coil3.network.NetworkHeaders
|
||||
import coil3.network.httpHeaders
|
||||
import coil3.request.CachePolicy
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.allowHardware
|
||||
import coil3.size.Size
|
||||
import com.hippo.ehviewer.BuildConfig
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.CHROME_ACCEPT
|
||||
import com.hippo.ehviewer.client.CHROME_ACCEPT_LANGUAGE
|
||||
import com.hippo.ehviewer.client.data.GalleryDetail
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.client.data.GalleryPreview
|
||||
import com.hippo.ehviewer.client.data.LargeGalleryPreview
|
||||
import com.hippo.ehviewer.client.data.NormalGalleryPreview
|
||||
import com.hippo.ehviewer.client.imageKey
|
||||
import com.hippo.ehviewer.spider.DownloadInfoMagics.encodeMagicRequestOrUrl
|
||||
@@ -34,11 +38,24 @@ fun ImageRequest.Builder.ehUrl(info: GalleryInfo) = apply {
|
||||
httpHeaders(header)
|
||||
}
|
||||
|
||||
fun ImageRequest.Builder.ehPreview(preview: GalleryPreview) = apply {
|
||||
data(preview.url)
|
||||
memoryCacheKey(preview.imageKey)
|
||||
diskCacheKey(preview.imageKey)
|
||||
if (preview is NormalGalleryPreview) size(Size.ORIGINAL)
|
||||
context(GalleryDetail)
|
||||
fun ImageRequest.Builder.ehPreview(
|
||||
preview: GalleryPreview,
|
||||
) = with(preview) {
|
||||
data(url)
|
||||
memoryCacheKey(imageKey)
|
||||
diskCacheKey(imageKey)
|
||||
when (preview) {
|
||||
is NormalGalleryPreview -> size(Size.ORIGINAL)
|
||||
is LargeGalleryPreview -> {
|
||||
if (hasAds && detectAds(position, pages, BuildConfig.DEBUG)) {
|
||||
size(Size.ORIGINAL)
|
||||
detectQrCode(true)
|
||||
allowHardware(false)
|
||||
hardwareThreshold(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
httpHeaders(header)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
package com.hippo.ehviewer.coil
|
||||
|
||||
import androidx.collection.SieveCache
|
||||
import coil3.Extras
|
||||
import coil3.Image
|
||||
import coil3.getExtra
|
||||
import coil3.intercept.Interceptor
|
||||
import coil3.memory.MemoryCache
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.ImageResult
|
||||
import coil3.request.SuccessResult
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.image.hasQrCode
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
|
||||
private val detectQrCodeKey = Extras.Key(default = false)
|
||||
|
||||
@@ -14,6 +20,8 @@ fun ImageRequest.Builder.detectQrCode(enable: Boolean) = apply {
|
||||
extras[detectQrCodeKey] = enable
|
||||
}
|
||||
|
||||
private val lruCache = SieveCache<MemoryCache.Key, Boolean>(50)
|
||||
|
||||
val ImageRequest.detectQrCode: Boolean
|
||||
get() = getExtra(detectQrCodeKey)
|
||||
|
||||
@@ -23,7 +31,11 @@ object QrCodeInterceptor : Interceptor {
|
||||
if (chain.request.detectQrCode && result is SuccessResult) {
|
||||
val image = result.image
|
||||
if (image is BitmapImageWithExtraInfo) {
|
||||
val hasQrCode = hasQrCode(image.image.bitmap)
|
||||
fun compute() = runSuspendCatching { hasQrCode(image.image.bitmap) }.onFailure { logcat(it) }.getOrThrow()
|
||||
val hasQrCode = when (val key = result.memoryCacheKey) {
|
||||
is MemoryCache.Key -> lruCache[key] ?: compute().also { lruCache[key] = it }
|
||||
null -> compute()
|
||||
}
|
||||
val new = image.copy(hasQrCode = hasQrCode)
|
||||
return result.copy(image = new)
|
||||
}
|
||||
@@ -31,3 +43,11 @@ object QrCodeInterceptor : Interceptor {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
val Image.hasQrCode
|
||||
get() = when (this) {
|
||||
is BitmapImageWithExtraInfo -> hasQrCode
|
||||
else -> false
|
||||
}
|
||||
|
||||
fun detectAds(index: Int, size: Int, enable: Boolean = true) = index > size - 10 && Settings.stripExtraneousAds.value && enable
|
||||
@@ -16,8 +16,8 @@
|
||||
package com.hippo.ehviewer.gallery
|
||||
|
||||
import android.os.ParcelFileDescriptor
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.Settings.archivePasswds
|
||||
import com.hippo.ehviewer.coil.detectAds
|
||||
import com.hippo.ehviewer.image.ByteBufferSource
|
||||
import com.hippo.ehviewer.image.Image
|
||||
import com.hippo.ehviewer.jni.closeArchive
|
||||
@@ -73,7 +73,6 @@ class ArchivePageLoader(
|
||||
}
|
||||
|
||||
override var size = 0
|
||||
private set
|
||||
|
||||
override fun start() {
|
||||
super.start()
|
||||
@@ -87,17 +86,17 @@ class ArchivePageLoader(
|
||||
logcat(DEBUG_TAG) { "Close archive ${file.toUri().displayPath} successfully!" }
|
||||
}
|
||||
|
||||
private val mJobMap = hashMapOf<Int, Job>()
|
||||
private val mWorkerMutex = NamedMutex<Int>()
|
||||
private val mSemaphore = Semaphore(4)
|
||||
private val jobs = hashMapOf<Int, Job>()
|
||||
private val workerMutex = NamedMutex<Int>()
|
||||
private val semaphore = Semaphore(4)
|
||||
|
||||
override fun onRequest(index: Int) {
|
||||
synchronized(mJobMap) {
|
||||
val current = mJobMap[index]
|
||||
synchronized(jobs) {
|
||||
val current = jobs[index]
|
||||
if (current?.isActive != true) {
|
||||
mJobMap[index] = launch {
|
||||
mWorkerMutex.withLock(index) {
|
||||
mSemaphore.withPermit {
|
||||
jobs[index] = launch {
|
||||
workerMutex.withLock(index) {
|
||||
semaphore.withPermit {
|
||||
doRealWork(index)
|
||||
}
|
||||
}
|
||||
@@ -106,8 +105,6 @@ class ArchivePageLoader(
|
||||
}
|
||||
}
|
||||
|
||||
private fun mayBeAd(index: Int) = index > size - 10
|
||||
|
||||
private suspend fun doRealWork(index: Int) {
|
||||
val buffer = extractToByteBuffer(index) ?: return notifyPageFailed(index, null)
|
||||
check(buffer.isDirect)
|
||||
@@ -124,7 +121,7 @@ class ArchivePageLoader(
|
||||
src.close()
|
||||
throw it
|
||||
}
|
||||
val image = Image.decode(src, hasAds && Settings.stripExtraneousAds.value && mayBeAd(index)) ?: return notifyPageFailed(index, null)
|
||||
val image = Image.decode(src, hasAds && detectAds(index, size)) ?: return notifyPageFailed(index, null)
|
||||
runCatching {
|
||||
currentCoroutineContext().ensureActive()
|
||||
}.onFailure {
|
||||
|
||||
+1
-5
@@ -1,12 +1,8 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.loader
|
||||
package com.hippo.ehviewer.gallery
|
||||
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.collection.lruCache
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.gallery.Page
|
||||
import com.hippo.ehviewer.gallery.PageStatus
|
||||
import com.hippo.ehviewer.gallery.reset
|
||||
import com.hippo.ehviewer.gallery.status
|
||||
import com.hippo.ehviewer.image.Image
|
||||
import com.hippo.ehviewer.util.OSUtils
|
||||
import com.hippo.ehviewer.util.isAtLeastO
|
||||
@@ -17,7 +17,6 @@ package com.hippo.ehviewer.gallery
|
||||
|
||||
import com.hippo.ehviewer.EhDB
|
||||
import com.hippo.ehviewer.util.FileUtils
|
||||
import eu.kanade.tachiyomi.ui.reader.loader.PageLoader
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
@@ -37,6 +37,7 @@ import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.coil.BitmapImageWithExtraInfo
|
||||
import com.hippo.ehviewer.coil.detectQrCode
|
||||
import com.hippo.ehviewer.coil.hardwareThreshold
|
||||
import com.hippo.ehviewer.coil.hasQrCode
|
||||
import com.hippo.ehviewer.coil.maybeCropBorder
|
||||
import com.hippo.ehviewer.jni.isGif
|
||||
import com.hippo.ehviewer.jni.mmap
|
||||
@@ -55,10 +56,7 @@ import splitties.init.appCtx
|
||||
class Image private constructor(image: CoilImage, private val src: ImageSource) {
|
||||
val intrinsicSize = with(image) { IntSize(width, height) }
|
||||
val allocationSize = image.size
|
||||
val hasQrCode = when (image) {
|
||||
is BitmapImageWithExtraInfo -> image.hasQrCode
|
||||
else -> false
|
||||
}
|
||||
val hasQrCode = image.hasQrCode
|
||||
|
||||
var innerImage: CoilImage? = when (image) {
|
||||
is BitmapImageWithExtraInfo -> image.image
|
||||
|
||||
@@ -5,6 +5,7 @@ import coil3.ImageLoader
|
||||
import coil3.disk.DiskCache
|
||||
import coil3.imageLoader
|
||||
import coil3.request.ImageRequest
|
||||
import com.hippo.ehviewer.client.data.GalleryDetail
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.client.data.GalleryPreview
|
||||
import com.hippo.ehviewer.coil.ehPreview
|
||||
@@ -12,6 +13,7 @@ import com.hippo.ehviewer.coil.ehUrl
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
context(GalleryDetail)
|
||||
inline fun Context.imageRequest(preview: GalleryPreview, builder: ImageRequest.Builder.() -> Unit = {}) = ImageRequest.Builder(this).ehPreview(preview).apply(builder).build()
|
||||
inline fun Context.imageRequest(info: GalleryInfo, builder: ImageRequest.Builder.() -> Unit = {}) = ImageRequest.Builder(this).ehUrl(info).apply(builder).build()
|
||||
inline fun Context.imageRequest(builder: ImageRequest.Builder.() -> Unit = {}) = ImageRequest.Builder(this).apply(builder).build()
|
||||
|
||||
@@ -17,8 +17,6 @@
|
||||
package com.hippo.ehviewer.spider
|
||||
|
||||
import androidx.annotation.IntDef
|
||||
import androidx.collection.LongSparseArray
|
||||
import androidx.collection.set
|
||||
import arrow.core.partially1
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
@@ -28,7 +26,6 @@ import com.hippo.ehviewer.client.EhUrl.getGalleryDetailUrl
|
||||
import com.hippo.ehviewer.client.EhUrl.getGalleryMultiPageViewerUrl
|
||||
import com.hippo.ehviewer.client.EhUrl.referer
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.client.data.hasAds
|
||||
import com.hippo.ehviewer.client.ehRequest
|
||||
import com.hippo.ehviewer.client.exception.QuotaExceededException
|
||||
import com.hippo.ehviewer.client.fetchUsingAsText
|
||||
@@ -37,6 +34,7 @@ import com.hippo.ehviewer.client.parser.GalleryDetailParser.parsePreviewList
|
||||
import com.hippo.ehviewer.client.parser.GalleryDetailParser.parsePreviewPages
|
||||
import com.hippo.ehviewer.client.parser.GalleryMultiPageViewerPTokenParser
|
||||
import com.hippo.ehviewer.client.parser.GalleryPageUrlParser
|
||||
import com.hippo.ehviewer.coil.detectAds
|
||||
import com.hippo.ehviewer.image.Image
|
||||
import com.hippo.ehviewer.util.displayString
|
||||
import com.hippo.files.find
|
||||
@@ -473,11 +471,11 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
|
||||
const val STATE_FINISHED = 2
|
||||
const val STATE_FAILED = 3
|
||||
const val SPIDER_INFO_FILENAME = ".ehviewer"
|
||||
private val sQueenMap = LongSparseArray<SpiderQueen>()
|
||||
private val sQueenMap = mutableMapOf<Long, SpiderQueen>()
|
||||
|
||||
fun obtainSpiderQueen(galleryInfo: GalleryInfo, @Mode mode: Int): SpiderQueen {
|
||||
val gid = galleryInfo.gid
|
||||
return (sQueenMap[gid] ?: SpiderQueen(galleryInfo).also { sQueenMap[gid] = it }).apply {
|
||||
return (sQueenMap.getOrPut(gid) { SpiderQueen(galleryInfo) }).apply {
|
||||
setMode(mode)
|
||||
launch { updateMode() }
|
||||
}
|
||||
@@ -740,13 +738,11 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
|
||||
}
|
||||
}
|
||||
|
||||
private fun mayBeAd(index: Int) = index > size - 10
|
||||
|
||||
private val hasAds = galleryInfo.hasAds
|
||||
|
||||
private suspend fun doInJob(index: Int) {
|
||||
val src = mSpiderDen.getImageSource(index) ?: return
|
||||
val image = Image.decode(src, hasAds && Settings.stripExtraneousAds.value && mayBeAd(index))
|
||||
val image = Image.decode(src, hasAds && detectAds(index, size))
|
||||
checkNotNull(image)
|
||||
runCatching {
|
||||
currentCoroutineContext().ensureActive()
|
||||
|
||||
@@ -10,6 +10,7 @@ import androidx.compose.runtime.NonRestartableComposable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -23,12 +24,16 @@ import coil3.BitmapImage
|
||||
import coil3.compose.AsyncImage
|
||||
import coil3.compose.AsyncImagePainter
|
||||
import coil3.request.ImageRequest
|
||||
import com.hippo.ehviewer.client.data.GalleryDetail
|
||||
import com.hippo.ehviewer.client.data.GalleryPreview
|
||||
import com.hippo.ehviewer.client.data.NormalGalleryPreview
|
||||
import com.hippo.ehviewer.coil.hasQrCode
|
||||
import com.hippo.ehviewer.ktbuilder.imageRequest
|
||||
import com.hippo.ehviewer.ui.reader.AdsPlaceholder
|
||||
import com.hippo.ehviewer.ui.tools.CrystalCard
|
||||
import com.hippo.ehviewer.ui.tools.shouldCrop
|
||||
|
||||
context(GalleryDetail)
|
||||
@Composable
|
||||
@NonRestartableComposable
|
||||
fun requestOf(model: GalleryPreview): ImageRequest {
|
||||
@@ -36,50 +41,60 @@ fun requestOf(model: GalleryPreview): ImageRequest {
|
||||
return remember(model) { context.imageRequest(model) }
|
||||
}
|
||||
|
||||
context(GalleryDetail)
|
||||
@Composable
|
||||
fun EhAsyncPreview(
|
||||
model: GalleryPreview,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var contentScale by remember(model) { mutableStateOf(ContentScale.Fit) }
|
||||
AsyncImage(
|
||||
model = requestOf(model),
|
||||
contentDescription = null,
|
||||
modifier = modifier,
|
||||
transform = {
|
||||
model.run {
|
||||
if (it is AsyncImagePainter.State.Success && this is NormalGalleryPreview) {
|
||||
it.copy(
|
||||
painter = BitmapPainter(
|
||||
(it.result.image as BitmapImage).bitmap.asImageBitmap(),
|
||||
IntOffset(offsetX, 0),
|
||||
IntSize(clipWidth - 1, clipHeight - 1),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
it
|
||||
var blocked by rememberSaveable(model) { mutableStateOf(false) }
|
||||
if (blocked) {
|
||||
AdsPlaceholder(
|
||||
modifier = modifier,
|
||||
contentScale = ContentScale.Fit,
|
||||
)
|
||||
} else {
|
||||
AsyncImage(
|
||||
model = requestOf(model),
|
||||
contentDescription = null,
|
||||
modifier = modifier,
|
||||
transform = {
|
||||
with(model) {
|
||||
when (it) {
|
||||
is AsyncImagePainter.State.Success if it.result.image.hasQrCode -> it.also { blocked = true }
|
||||
is AsyncImagePainter.State.Success if this is NormalGalleryPreview -> it.copy(
|
||||
painter = BitmapPainter(
|
||||
(it.result.image as BitmapImage).bitmap.asImageBitmap(),
|
||||
IntOffset(offsetX, 0),
|
||||
IntSize(clipWidth - 1, clipHeight - 1),
|
||||
),
|
||||
)
|
||||
else -> it
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onState = {
|
||||
if (it is AsyncImagePainter.State.Success) {
|
||||
model.run {
|
||||
if (this is NormalGalleryPreview) {
|
||||
if (shouldCrop) {
|
||||
contentScale = ContentScale.Crop
|
||||
}
|
||||
} else {
|
||||
if (it.result.image.shouldCrop) {
|
||||
contentScale = ContentScale.Crop
|
||||
},
|
||||
onState = {
|
||||
if (it is AsyncImagePainter.State.Success) {
|
||||
with(model) {
|
||||
if (this is NormalGalleryPreview) {
|
||||
if (shouldCrop) {
|
||||
contentScale = ContentScale.Crop
|
||||
}
|
||||
} else {
|
||||
if (it.result.image.shouldCrop) {
|
||||
contentScale = ContentScale.Crop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
contentScale = contentScale,
|
||||
)
|
||||
},
|
||||
contentScale = contentScale,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
context(GalleryDetail)
|
||||
@Composable
|
||||
@NonRestartableComposable
|
||||
fun EhPreviewItem(
|
||||
|
||||
@@ -35,12 +35,12 @@ import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.collectAsState
|
||||
import com.hippo.ehviewer.gallery.Page
|
||||
import com.hippo.ehviewer.gallery.PageLoader
|
||||
import com.hippo.ehviewer.gallery.PageStatus
|
||||
import com.hippo.ehviewer.gallery.progressObserved
|
||||
import com.hippo.ehviewer.gallery.statusObserved
|
||||
import com.hippo.ehviewer.image.Image
|
||||
import com.hippo.ehviewer.ui.settings.AdsPlaceholderFile
|
||||
import eu.kanade.tachiyomi.ui.reader.loader.PageLoader
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.CombinedCircularProgressIndicator
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import moe.tarsin.kt.unreachable
|
||||
@@ -101,26 +101,10 @@ fun PagerItem(
|
||||
)
|
||||
}
|
||||
is PageStatus.Blocked -> {
|
||||
SubcomposeAsyncImage(
|
||||
model = AdsPlaceholderFile,
|
||||
contentDescription = null,
|
||||
AdsPlaceholder(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
contentScale = if (contentScale == ContentScale.Inside) ContentScale.Fit else contentScale,
|
||||
) {
|
||||
val placeholderState by painter.state.collectAsState()
|
||||
if (placeholderState is AsyncImagePainter.State.Success) {
|
||||
SubcomposeAsyncImageContent()
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth().aspectRatio(DEFAULT_ASPECT),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
if (placeholderState is AsyncImagePainter.State.Error) {
|
||||
Text(text = stringResource(id = R.string.blocked_image))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
is PageStatus.Error -> {
|
||||
Box(modifier = modifier.fillMaxWidth().aspectRatio(DEFAULT_ASPECT)) {
|
||||
@@ -168,3 +152,28 @@ private val grayScaleAndInvertMatrix = ColorMatrix().also { mtx ->
|
||||
private val grayScaleFilter = ColorFilter.colorMatrix(grayScaleMatrix)
|
||||
private val invertFilter = ColorFilter.colorMatrix(invertMatrix)
|
||||
private val grayScaleAndInvertFilter = ColorFilter.colorMatrix(grayScaleAndInvertMatrix)
|
||||
|
||||
@Composable
|
||||
fun AdsPlaceholder(
|
||||
modifier: Modifier = Modifier,
|
||||
contentScale: ContentScale,
|
||||
) = SubcomposeAsyncImage(
|
||||
model = AdsPlaceholderFile,
|
||||
contentDescription = null,
|
||||
modifier = modifier,
|
||||
contentScale = contentScale,
|
||||
) {
|
||||
val placeholderState by painter.state.collectAsState()
|
||||
if (placeholderState is AsyncImagePainter.State.Success) {
|
||||
SubcomposeAsyncImageContent()
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth().aspectRatio(DEFAULT_ASPECT),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
if (placeholderState is AsyncImagePainter.State.Error) {
|
||||
Text(text = stringResource(id = R.string.blocked_image))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,6 @@ import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
||||
import com.hippo.ehviewer.client.data.hasAds
|
||||
import com.hippo.ehviewer.collectAsState
|
||||
import com.hippo.ehviewer.download.DownloadManager
|
||||
import com.hippo.ehviewer.download.archiveFile
|
||||
|
||||
@@ -747,8 +747,10 @@ private fun List<GalleryTagGroup>.getArtistTag(): String? {
|
||||
return null
|
||||
}
|
||||
|
||||
typealias Previews = Pair<GalleryDetail, LazyPagingItems<GalleryPreview>>
|
||||
|
||||
@Composable
|
||||
private fun Context.collectPreviewItems(detail: GalleryDetail, prefetchDistance: Int) = rememberInVM {
|
||||
private fun Context.collectPreviewItems(detail: GalleryDetail, prefetchDistance: Int) = rememberInVM(detail) {
|
||||
val pageSize = detail.previewList.size
|
||||
val pages = detail.pages
|
||||
val previewPagesMap = detail.previewList.associateBy { it.position } as MutableMap
|
||||
@@ -787,15 +789,18 @@ private fun Context.collectPreviewItems(detail: GalleryDetail, prefetchDistance:
|
||||
override val jumpingSupported = true
|
||||
}
|
||||
}.flow.cachedIn(viewModelScope)
|
||||
}.collectAsLazyPagingItems()
|
||||
}.collectAsLazyPagingItems().let { remember(detail, it) { detail to it } }
|
||||
|
||||
private fun LazyGridScope.galleryPreview(data: LazyPagingItems<GalleryPreview>, onClick: (Int) -> Unit) {
|
||||
private fun LazyGridScope.galleryPreview(previews: Previews, onClick: (Int) -> Unit) {
|
||||
val (detail, data) = previews
|
||||
items(
|
||||
count = data.itemCount,
|
||||
key = data.itemKey(key = { item -> item.position }),
|
||||
contentType = { "preview" },
|
||||
) { index ->
|
||||
val item = data[index]
|
||||
EhPreviewItem(item, index) { onClick(index) }
|
||||
with(detail) {
|
||||
EhPreviewItem(item, index) { onClick(index) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -150,8 +150,10 @@ fun AnimatedVisibilityScope.GalleryDetailScreen(args: GalleryDetailScreenArgs, n
|
||||
galleryDetailCache.put(galleryDetail.gid, galleryDetail)
|
||||
if (Settings.preloadThumbAggressively) {
|
||||
launchIO {
|
||||
galleryDetail.previewList.forEach {
|
||||
imageLoader.enqueue(imageRequest(it) { justDownload() })
|
||||
with(galleryDetail) {
|
||||
previewList.forEach {
|
||||
imageLoader.enqueue(imageRequest(it) { justDownload() })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,7 +221,7 @@ fun AnimatedVisibilityScope.GalleryListScreen(lub: ListUrlBuilder, navigator: De
|
||||
override suspend fun load(params: LoadParams<String>) = withIOContext {
|
||||
if (urlBuilder.mode == MODE_TOPLIST) {
|
||||
// TODO: Since we know total pages, let pager support jump
|
||||
val key = (params.key ?: urlBuilder.mJumpTo)?.toInt() ?: 0
|
||||
val key = (params.key ?: urlBuilder.jumpTo)?.toInt() ?: 0
|
||||
val prev = (key - 1).takeIf { it > 0 }
|
||||
val next = (key + 1).takeIf { it < TOPLIST_PAGES }
|
||||
runSuspendCatching {
|
||||
@@ -237,8 +237,8 @@ fun AnimatedVisibilityScope.GalleryListScreen(lub: ListUrlBuilder, navigator: De
|
||||
is LoadParams.Refresh -> {
|
||||
val key = params.key
|
||||
if (key.isNullOrBlank()) {
|
||||
if (urlBuilder.mJumpTo != null) {
|
||||
urlBuilder.mNext ?: urlBuilder.setIndex("2", true)
|
||||
if (urlBuilder.jumpTo != null) {
|
||||
urlBuilder.next ?: urlBuilder.setIndex("2", true)
|
||||
}
|
||||
} else {
|
||||
urlBuilder.setIndex(key, false)
|
||||
@@ -249,7 +249,7 @@ fun AnimatedVisibilityScope.GalleryListScreen(lub: ListUrlBuilder, navigator: De
|
||||
val url = urlBuilder.build()
|
||||
EhEngine.getGalleryList(url)
|
||||
}.foldToLoadResult { result ->
|
||||
urlBuilder.mJumpTo = null
|
||||
urlBuilder.jumpTo = null
|
||||
LoadResult.Page(result.galleryInfoList, result.prev, result.next)
|
||||
}
|
||||
}
|
||||
@@ -736,7 +736,7 @@ fun AnimatedVisibilityScope.GalleryListScreen(lub: ListUrlBuilder, navigator: De
|
||||
if (urlBuilder.mode != MODE_WHATS_HOT) {
|
||||
onClick(EhIcons.Default.GoTo) {
|
||||
if (isTopList) {
|
||||
val page = urlBuilder.mJumpTo?.toIntOrNull() ?: 0
|
||||
val page = urlBuilder.jumpTo?.toIntOrNull() ?: 0
|
||||
val hint = getString(R.string.go_to_hint, page + 1, TOPLIST_PAGES)
|
||||
val text = awaitInputText(title = gotoTitle, hint = hint, isNumber = true) { oriText ->
|
||||
val goto = ensureNotNull(oriText.trim().toIntOrNull()) { invalidNum } - 1
|
||||
@@ -745,7 +745,7 @@ fun AnimatedVisibilityScope.GalleryListScreen(lub: ListUrlBuilder, navigator: De
|
||||
urlBuilder.setJumpTo(text)
|
||||
} else {
|
||||
val date = awaitSelectDate()
|
||||
urlBuilder.mJumpTo = date
|
||||
urlBuilder.jumpTo = date
|
||||
}
|
||||
data.refresh()
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ inline fun <reified T : Any> rememberInVM(
|
||||
) = with(viewModel<StateMapViewModel>()) {
|
||||
val key = currentCompositeKeyHash
|
||||
remember(*inputs) {
|
||||
val states = statesMap[key] ?: ArrayDeque<Any>().also { statesMap[key] = it }
|
||||
val states = statesMap.getOrPut(key) { ArrayDeque() }
|
||||
states.removeLastOrNull() as T? ?: init()
|
||||
}.also { value ->
|
||||
val valueState by rememberUpdatedState(value)
|
||||
|
||||
@@ -91,7 +91,7 @@ jsoup = "org.jsoup:jsoup:1.18.1"
|
||||
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }
|
||||
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
|
||||
|
||||
ktlint = "com.pinterest:ktlint:1.3.0"
|
||||
ktlint = "com.pinterest:ktlint:1.4.0-kotlin-dev-20240924.151646-39"
|
||||
|
||||
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ dependencyResolutionManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven("https://oss.sonatype.org/content/repositories/snapshots")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user