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:
renovate[bot]andDude so hot authored and GitHub committed 2024-09-30 21:09:56 +08:00
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,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)
+1 -1
View File
@@ -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" }
+1
View File
@@ -11,6 +11,7 @@ dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven("https://oss.sonatype.org/content/repositories/snapshots")
}
}