Fuckoff External Ads (#1617)
* Add Rust implementation to detect QRCode from bitmap * Add QRCodeInterceptor Framework * Let image take hasQRCode info * Keep ReaderPage index internal * Move imageCache to use ReaderPage as key * Make PageLoader size & pages internal * Refine * Kill entry if QRCode * Rewrite request to respect index mapping * Debug: enable by default * Fix crash * Fixup * Fix identity * Revert to lruCache to avoid crash * Fixup * Clippy check for android only * Fixup * Resolve clippy * Switch * Apply switch * Restart pageloader restore pages * Cleanup * Cleanup Signed-off-by: Dude so hot <djohn@fbi.gov> * let it inference Signed-off-by: Dude so hot <djohn@fbi.gov> * Don't detect border for ads * Make coil interceptor runs in `Dispatchers.Default` * Revert "Restart pageloader restore pages" This reverts commit 8fd02fb7d9c677c119df72a767a6079f1899dc31. * Revert drop page changes * Use backGround color painter for ads * Only check ads for tags contains extraneous ads * Only check ads for head 5 images || tail 10 images * Move strip ADs switch to AdvancedScreen * Pick placeholder * Show Placeholder * Don't try to extract & decode Signed-off-by: Dude so hot <djohn@fbi.gov> * cleanup Signed-off-by: Dude so hot <djohn@fbi.gov> * Naming * Naming * Naming * Naming * For ArchivePageLoader as well * Hint * Fix flicker * Refine * Naming * QR -> Qr * Naming * QR -> Qr * Naming * Refine * Revert * Remove header check * Allow show ads * Adjust * Refine * Revert "Don't try to extract & decode" This reverts commit f35ef46f6ffc81485cf0f4c646ed79e524c29d32. Signed-off-by: Dude so hot <djohn@fbi.gov> * be strict Signed-off-by: Dude so hot <djohn@fbi.gov> * Just Extract, don't decode * Refine * Refine placeholder --------- Signed-off-by: Dude so hot <djohn@fbi.gov> Co-authored-by: Dude so hot <djohn@fbi.gov> Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com>
This commit is contained in:
26 files changed
+730
-79
No files matched your search
@@ -35,7 +35,7 @@ jobs:
|
||||
|
||||
- name: Clippy Check
|
||||
working-directory: ./app/src/main/rust
|
||||
run: cargo clippy --all-targets --all-features -- -D warnings
|
||||
run: cargo clippy --target aarch64-linux-android --all-features -- -D warnings
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
|
||||
@@ -42,7 +42,9 @@ import com.hippo.ehviewer.client.data.GalleryDetail
|
||||
import com.hippo.ehviewer.coil.CropBorderInterceptor
|
||||
import com.hippo.ehviewer.coil.DownloadThumbInterceptor
|
||||
import com.hippo.ehviewer.coil.HardwareBitmapInterceptor
|
||||
import com.hippo.ehviewer.coil.MapExtraInfoInterceptor
|
||||
import com.hippo.ehviewer.coil.MergeInterceptor
|
||||
import com.hippo.ehviewer.coil.QrCodeInterceptor
|
||||
import com.hippo.ehviewer.cronet.cronetHttpClient
|
||||
import com.hippo.ehviewer.dailycheck.checkDawn
|
||||
import com.hippo.ehviewer.dao.SearchDatabase
|
||||
@@ -68,6 +70,7 @@ import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.plugins.cookies.HttpCookies
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import logcat.AndroidLogcatLogger
|
||||
import logcat.LogPriority
|
||||
@@ -169,6 +172,7 @@ class EhApplication :
|
||||
}
|
||||
|
||||
override fun newImageLoader(context: Context) = context.imageLoader {
|
||||
interceptorCoroutineContext(Dispatchers.Default)
|
||||
components {
|
||||
serviceLoaderEnabled(false)
|
||||
add(KtorNetworkFetcherFactory { ktorClient })
|
||||
@@ -178,6 +182,8 @@ class EhApplication :
|
||||
add(HardwareBitmapInterceptor)
|
||||
}
|
||||
add(CropBorderInterceptor)
|
||||
add(QrCodeInterceptor)
|
||||
add(MapExtraInfoInterceptor)
|
||||
if (isAtLeastP) {
|
||||
add(AnimatedImageDecoder.Factory(false))
|
||||
} else {
|
||||
|
||||
@@ -209,6 +209,7 @@ object Settings : DataStorePreferences(null) {
|
||||
val zoomStart = intPref("pref_zoom_start_key", 1)
|
||||
val showNavigationOverlayNewUser = boolPref("reader_navigation_overlay_new_user", true)
|
||||
val showNavigationOverlayOnStart = boolPref("reader_navigation_overlay_on_start", false)
|
||||
val stripExtraneousAds = boolPref("strip_extraneous_ads", false)
|
||||
|
||||
val userAgent
|
||||
get() = if (desktopSite) CHROME_USER_AGENT else CHROME_MOBILE_USER_AGENT
|
||||
|
||||
@@ -141,3 +141,6 @@ fun GalleryInfo.findBaseInfo(): BaseGalleryInfo = when (this) {
|
||||
}
|
||||
|
||||
fun GalleryInfo.asGalleryDetail(): GalleryDetail? = this as? GalleryDetail
|
||||
|
||||
val GalleryInfo.hasAds
|
||||
get() = simpleTags?.any { "extraneous ads" in it } ?: false
|
||||
@@ -1,9 +1,7 @@
|
||||
package com.hippo.ehviewer.coil
|
||||
|
||||
import androidx.compose.ui.unit.IntRect
|
||||
import coil3.BitmapImage
|
||||
import coil3.Extras
|
||||
import coil3.Image
|
||||
import coil3.getExtra
|
||||
import coil3.intercept.Interceptor
|
||||
import coil3.intercept.Interceptor.Chain
|
||||
@@ -24,18 +22,13 @@ fun ImageRequest.Builder.maybeCropBorder(enable: Boolean) = apply {
|
||||
val ImageRequest.maybeCropBorder: Boolean
|
||||
get() = getExtra(maybeCropBorderKey)
|
||||
|
||||
class BitmapImageWithRect(
|
||||
val rect: IntRect,
|
||||
val image: BitmapImage,
|
||||
) : Image by image
|
||||
|
||||
object CropBorderInterceptor : Interceptor {
|
||||
override suspend fun intercept(chain: Chain): ImageResult {
|
||||
val result = chain.proceed()
|
||||
if (chain.request.maybeCropBorder && result is SuccessResult) {
|
||||
val image = result.image
|
||||
if (image is BitmapImage) {
|
||||
val bitmap = image.bitmap
|
||||
if (image is BitmapImageWithExtraInfo && !image.hasQrCode) {
|
||||
val bitmap = image.image.bitmap
|
||||
val ratio = if (image.height > image.width) {
|
||||
image.height / image.width
|
||||
} else {
|
||||
@@ -47,8 +40,8 @@ object CropBorderInterceptor : Interceptor {
|
||||
val minHeight = image.height * CROP_THRESHOLD
|
||||
val rect = IntRect(array[0], array[1], array[2], array[3])
|
||||
if (rect.width > minWidth && rect.height > minHeight) {
|
||||
val new = BitmapImageWithRect(rect, image)
|
||||
return result.copy(image = new, request = result.request, dataSource = result.dataSource)
|
||||
val new = image.copy(rect = rect)
|
||||
return result.copy(image = new)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@ package com.hippo.ehviewer.coil
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.ui.unit.IntRect
|
||||
import coil3.BitmapImage
|
||||
import coil3.Extras
|
||||
import coil3.asImage
|
||||
import coil3.getExtra
|
||||
@@ -30,25 +28,18 @@ object HardwareBitmapInterceptor : Interceptor {
|
||||
val result = chain.proceed()
|
||||
val request = result.request
|
||||
if (!request.allowHardware && result is SuccessResult) {
|
||||
var rect: IntRect? = null
|
||||
val image = result.image
|
||||
val bitmap = when (image) {
|
||||
is BitmapImageWithRect -> image.image.also { rect = image.rect }
|
||||
is BitmapImage -> image
|
||||
else -> return result
|
||||
}.bitmap
|
||||
// Large hardware bitmaps have rendering issues (e.g. crash, empty) on some devices.
|
||||
// This is not ideal but I haven't figured out how to probe the threshold.
|
||||
// All we know is that it's less than the maximum texture size.
|
||||
if (maxOf(bitmap.width, bitmap.height) <= request.hardwareThreshold) {
|
||||
bitmap.copy(Bitmap.Config.HARDWARE, false)?.let { hwBitmap ->
|
||||
bitmap.recycle()
|
||||
val newImage = hwBitmap.asImage()
|
||||
return result.copy(
|
||||
image = rect?.let { BitmapImageWithRect(it, newImage as BitmapImage) } ?: newImage,
|
||||
request = request,
|
||||
dataSource = result.dataSource,
|
||||
)
|
||||
if (image is BitmapImageWithExtraInfo) {
|
||||
val bitmap = image.image.bitmap
|
||||
// Large hardware bitmaps have rendering issues (e.g. crash, empty) on some devices.
|
||||
// This is not ideal but I haven't figured out how to probe the threshold.
|
||||
// All we know is that it's less than the maximum texture size.
|
||||
if (maxOf(bitmap.width, bitmap.height) <= request.hardwareThreshold) {
|
||||
bitmap.copy(Bitmap.Config.HARDWARE, false)?.let { hwBitmap ->
|
||||
bitmap.recycle()
|
||||
val newImage = hwBitmap.asImage()
|
||||
return result.copy(image = image.copy(image = newImage))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.hippo.ehviewer.coil
|
||||
|
||||
import androidx.compose.ui.unit.IntRect
|
||||
import coil3.BitmapImage
|
||||
import coil3.Image
|
||||
import coil3.intercept.Interceptor
|
||||
import coil3.request.ImageResult
|
||||
import coil3.request.SuccessResult
|
||||
import coil3.request.allowHardware
|
||||
|
||||
data class BitmapImageWithExtraInfo(
|
||||
val image: BitmapImage,
|
||||
val rect: IntRect = IntRect(0, 0, image.width, image.height),
|
||||
val hasQrCode: Boolean = false,
|
||||
) : Image by image
|
||||
|
||||
object MapExtraInfoInterceptor : Interceptor {
|
||||
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
|
||||
val result = chain.proceed()
|
||||
val needMap = with(chain.request) { maybeCropBorder || detectQrCode || !allowHardware }
|
||||
if (needMap && result is SuccessResult) {
|
||||
val image = result.image
|
||||
if (image is BitmapImage) {
|
||||
return result.copy(image = BitmapImageWithExtraInfo(image))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
object MergeInterceptor : Interceptor {
|
||||
private val pendingContinuationMap: HashMap<String, MutableList<CancellableContinuation<Unit>>> = hashMapOf()
|
||||
@@ -42,7 +41,7 @@ object MergeInterceptor : Interceptor {
|
||||
|
||||
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
|
||||
val req = chain.request
|
||||
val key = req.memoryCacheKey?.takeIf { it.isNormalPreviewKey || Settings.preloadThumbAggressively } ?: return withContext(req.interceptorCoroutineContext) { chain.proceed() }
|
||||
val key = req.memoryCacheKey?.takeIf { it.isNormalPreviewKey || Settings.preloadThumbAggressively } ?: return chain.proceed()
|
||||
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
synchronized(pendingContinuationMapLock) {
|
||||
@@ -63,7 +62,7 @@ object MergeInterceptor : Interceptor {
|
||||
}
|
||||
|
||||
try {
|
||||
return withContext(req.interceptorCoroutineContext) { chain.proceed() }.apply {
|
||||
return chain.proceed().apply {
|
||||
// Wake all pending continuations shared with the same memory key since we have written it to memory cache
|
||||
notifyScope.launch {
|
||||
synchronized(pendingContinuationMapLock) {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.hippo.ehviewer.coil
|
||||
|
||||
import coil3.Extras
|
||||
import coil3.getExtra
|
||||
import coil3.intercept.Interceptor
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.ImageResult
|
||||
import coil3.request.SuccessResult
|
||||
import com.hippo.ehviewer.image.hasQrCode
|
||||
|
||||
private val detectQrCodeKey = Extras.Key(default = false)
|
||||
|
||||
fun ImageRequest.Builder.detectQrCode(enable: Boolean) = apply {
|
||||
extras[detectQrCodeKey] = enable
|
||||
}
|
||||
|
||||
val ImageRequest.detectQrCode: Boolean
|
||||
get() = getExtra(detectQrCodeKey)
|
||||
|
||||
object QrCodeInterceptor : Interceptor {
|
||||
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
|
||||
val result = chain.proceed()
|
||||
if (chain.request.detectQrCode && result is SuccessResult) {
|
||||
val image = result.image
|
||||
if (image is BitmapImageWithExtraInfo) {
|
||||
val hasQrCode = hasQrCode(image.image.bitmap)
|
||||
val new = image.copy(hasQrCode = hasQrCode)
|
||||
return result.copy(image = new)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
package com.hippo.ehviewer.gallery
|
||||
|
||||
import android.os.ParcelFileDescriptor
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.Settings.archivePasswds
|
||||
import com.hippo.ehviewer.image.ByteBufferSource
|
||||
import com.hippo.ehviewer.image.Image
|
||||
@@ -52,6 +53,7 @@ class ArchivePageLoader(
|
||||
private val file: Path,
|
||||
gid: Long = 0,
|
||||
startPage: Int = 0,
|
||||
private val hasAds: Boolean = false,
|
||||
passwdProvider: PasswdProvider? = null,
|
||||
) : PageLoader2(gid, startPage) {
|
||||
private lateinit var pfd: ParcelFileDescriptor
|
||||
@@ -104,6 +106,8 @@ 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)
|
||||
@@ -120,7 +124,7 @@ class ArchivePageLoader(
|
||||
src.close()
|
||||
throw it
|
||||
}
|
||||
val image = Image.decode(src) ?: return notifyPageFailed(index, null)
|
||||
val image = Image.decode(src, hasAds && Settings.stripExtraneousAds.value && mayBeAd(index)) ?: return notifyPageFailed(index, null)
|
||||
runCatching {
|
||||
currentCoroutineContext().ensureActive()
|
||||
}.onFailure {
|
||||
@@ -142,10 +146,6 @@ class ArchivePageLoader(
|
||||
override val isReady: Boolean
|
||||
get() = size != 0
|
||||
|
||||
override fun onCancelRequest(index: Int) {
|
||||
mJobMap[index]?.cancel()
|
||||
}
|
||||
|
||||
override val title by lazy {
|
||||
FileUtils.getNameFromFilename(file.name)!!
|
||||
}
|
||||
|
||||
@@ -66,10 +66,6 @@ class EhPageLoader(private val mGalleryInfo: GalleryInfo, startPage: Int) :
|
||||
override val isReady: Boolean
|
||||
get() = ::mSpiderQueen.isInitialized && mSpiderQueen.isReady
|
||||
|
||||
override fun onCancelRequest(index: Int) {
|
||||
mSpiderQueen.cancelRequest(index)
|
||||
}
|
||||
|
||||
override fun onGetPages(pages: Int) {}
|
||||
|
||||
override fun onGet509(index: Int) {}
|
||||
|
||||
@@ -33,7 +33,8 @@ import coil3.request.allowHardware
|
||||
import coil3.size.Dimension
|
||||
import coil3.size.Precision
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.coil.BitmapImageWithRect
|
||||
import com.hippo.ehviewer.coil.BitmapImageWithExtraInfo
|
||||
import com.hippo.ehviewer.coil.detectQrCode
|
||||
import com.hippo.ehviewer.coil.hardwareThreshold
|
||||
import com.hippo.ehviewer.coil.maybeCropBorder
|
||||
import com.hippo.ehviewer.jni.isGif
|
||||
@@ -53,12 +54,16 @@ import splitties.init.appCtx
|
||||
class Image private constructor(image: CoilImage, private val src: ImageSource) {
|
||||
val size = image.size
|
||||
val rect = when (image) {
|
||||
is BitmapImageWithRect -> image.rect
|
||||
else -> image.run { IntRect(0, 0, width, height) }
|
||||
is BitmapImageWithExtraInfo -> image.rect
|
||||
else -> with(image) { IntRect(0, 0, width, height) }
|
||||
}
|
||||
val hasQrCode = when (image) {
|
||||
is BitmapImageWithExtraInfo -> image.hasQrCode
|
||||
else -> false
|
||||
}
|
||||
|
||||
var innerImage: CoilImage? = when (image) {
|
||||
is BitmapImageWithRect -> image.image
|
||||
is BitmapImageWithExtraInfo -> image.image
|
||||
else -> image
|
||||
}
|
||||
|
||||
@@ -76,7 +81,7 @@ class Image private constructor(image: CoilImage, private val src: ImageSource)
|
||||
companion object {
|
||||
private val targetWidth = appCtx.resources.displayMetrics.widthPixels * 3
|
||||
|
||||
private suspend fun Either<ByteBufferSource, PathSource>.decodeCoil(): CoilImage {
|
||||
private suspend fun Either<ByteBufferSource, PathSource>.decodeCoil(checkExtraneousAds: Boolean): CoilImage {
|
||||
val req = appCtx.imageRequest {
|
||||
onLeft { data(it.source) }
|
||||
onRight { data(it.source.toUri()) }
|
||||
@@ -85,6 +90,7 @@ class Image private constructor(image: CoilImage, private val src: ImageSource)
|
||||
allowHardware(false)
|
||||
hardwareThreshold(Settings.hardwareBitmapThreshold)
|
||||
maybeCropBorder(Settings.cropBorder.value)
|
||||
detectQrCode(checkExtraneousAds)
|
||||
memoryCachePolicy(CachePolicy.DISABLED)
|
||||
}
|
||||
return when (val result = appCtx.imageLoader.execute(req)) {
|
||||
@@ -93,7 +99,7 @@ class Image private constructor(image: CoilImage, private val src: ImageSource)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun decode(src: ImageSource): Image? {
|
||||
suspend fun decode(src: ImageSource, checkExtraneousAds: Boolean = false): Image? {
|
||||
return runCatching {
|
||||
val image = when (src) {
|
||||
is PathSource -> {
|
||||
@@ -109,18 +115,18 @@ class Image private constructor(image: CoilImage, private val src: ImageSource)
|
||||
src.close()
|
||||
}
|
||||
}
|
||||
return decode(source)
|
||||
return decode(source, checkExtraneousAds)
|
||||
}
|
||||
}
|
||||
}
|
||||
src.right().decodeCoil()
|
||||
src.right().decodeCoil(checkExtraneousAds)
|
||||
}
|
||||
|
||||
is ByteBufferSource -> {
|
||||
if (isAtLeastP && !isAtLeastU) {
|
||||
rewriteGifSource(src.source)
|
||||
}
|
||||
src.left().decodeCoil()
|
||||
src.left().decodeCoil(checkExtraneousAds)
|
||||
}
|
||||
}
|
||||
Image(image, src).apply {
|
||||
@@ -146,3 +152,4 @@ interface ByteBufferSource : ImageSource {
|
||||
}
|
||||
|
||||
external fun detectBorder(bitmap: Bitmap): IntArray
|
||||
external fun hasQrCode(bitmap: Bitmap): Boolean
|
||||
@@ -28,6 +28,7 @@ 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
|
||||
@@ -284,10 +285,6 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelRequest(index: Int) {
|
||||
mWorkerScope.cancelDecode(index)
|
||||
}
|
||||
|
||||
fun preloadPages(pages: List<Int>, pair: Pair<Int, Int>) {
|
||||
mWorkerScope.updateRAList(pages, pair)
|
||||
}
|
||||
@@ -512,10 +509,6 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
|
||||
var isDownloadMode = false
|
||||
private set
|
||||
|
||||
fun cancelDecode(index: Int) {
|
||||
decoder.cancel(index)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun enterDownloadMode() {
|
||||
if (isDownloadMode) return
|
||||
@@ -749,9 +742,13 @@ 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)
|
||||
val image = Image.decode(src, hasAds && Settings.stripExtraneousAds.value && mayBeAd(index))
|
||||
checkNotNull(image)
|
||||
runCatching {
|
||||
currentCoroutineContext().ensureActive()
|
||||
|
||||
@@ -26,11 +26,15 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil3.BitmapImage
|
||||
import coil3.DrawableImage
|
||||
import coil3.compose.AsyncImagePainter
|
||||
import coil3.compose.SubcomposeAsyncImage
|
||||
import coil3.compose.SubcomposeAsyncImageContent
|
||||
import com.google.accompanist.drawablepainter.DrawablePainter
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.collectAsState
|
||||
import com.hippo.ehviewer.image.Image
|
||||
import com.hippo.ehviewer.ui.settings.AdsPlaceholderFile
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.reader.loader.PageLoader
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
@@ -95,6 +99,27 @@ fun PagerItem(
|
||||
},
|
||||
)
|
||||
}
|
||||
Page.State.BLOCKED -> {
|
||||
SubcomposeAsyncImage(
|
||||
model = AdsPlaceholderFile,
|
||||
contentDescription = null,
|
||||
modifier = modifier.fillMaxSize(),
|
||||
) {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Page.State.ERROR -> {
|
||||
Box(modifier = modifier.fillMaxWidth().aspectRatio(DEFAULT_ASPECT)) {
|
||||
Column(
|
||||
|
||||
@@ -54,6 +54,7 @@ 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
|
||||
@@ -70,6 +71,7 @@ import com.hippo.files.toOkioPath
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.reader.PageIndicatorText
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderAppBars
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderContentOverlay
|
||||
@@ -79,6 +81,7 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import kotlin.coroutines.resume
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.sample
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -165,8 +168,10 @@ fun AnimatedVisibilityScope.ReaderScreen(pageLoader: PageLoader2, info: BaseGall
|
||||
.collect { setOrientation(it) }
|
||||
}
|
||||
LaunchedEffect(pageLoader) {
|
||||
Settings.cropBorder.changesFlow().collect {
|
||||
pageLoader.restart()
|
||||
with(Settings) {
|
||||
merge(cropBorder.changesFlow(), stripExtraneousAds.changesFlow()).collect {
|
||||
pageLoader.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
val showSeekbar by Settings.showReaderSeekbar.collectAsState()
|
||||
@@ -214,6 +219,7 @@ fun AnimatedVisibilityScope.ReaderScreen(pageLoader: PageLoader2, info: BaseGall
|
||||
val onSelectPage = { page: ReaderPage ->
|
||||
if (Settings.readerLongTapAction.value) {
|
||||
launch {
|
||||
val pageBlocked = page.status.value == Page.State.BLOCKED
|
||||
dialog { cont ->
|
||||
fun dispose() = cont.resume(Unit)
|
||||
val state = rememberModalBottomSheetState()
|
||||
@@ -230,6 +236,7 @@ fun AnimatedVisibilityScope.ReaderScreen(pageLoader: PageLoader2, info: BaseGall
|
||||
copy = { launchIO { with(pageLoader) { copy(page) } } },
|
||||
save = { launchIO { with(pageLoader) { save(page) } } },
|
||||
saveTo = { launchIO { with(pageLoader) { saveTo(page) } } },
|
||||
showAds = { page.status.value = Page.State.READY }.takeIf { pageBlocked },
|
||||
dismiss = { launch { state.hide().also { dispose() } } },
|
||||
)
|
||||
}
|
||||
@@ -345,7 +352,7 @@ private suspend fun preparePageLoader(args: ReaderScreenArgs) = when (args) {
|
||||
val page = args.page
|
||||
val archive = withIOContext { DownloadManager.getDownloadInfo(info.gid)?.archiveFile }
|
||||
if (archive != null) {
|
||||
ArchivePageLoader(archive, info.gid, page)
|
||||
ArchivePageLoader(archive, info.gid, page, info.hasAds)
|
||||
} else {
|
||||
EhPageLoader(info, page)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ import androidx.compose.ui.unit.dp
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.collectAsState
|
||||
import com.hippo.ehviewer.gallery.PageLoader2
|
||||
import com.hippo.ehviewer.ui.main.plus
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.NavigationRegions
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation.NavigationRegion
|
||||
|
||||
@@ -5,7 +5,10 @@ import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS
|
||||
import android.provider.Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS
|
||||
import androidx.activity.result.PickVisualMediaRequest
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.ImageOnly
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
@@ -39,6 +42,7 @@ import com.hippo.ehviewer.client.EhEngine
|
||||
import com.hippo.ehviewer.client.data.FavListUrlBuilder
|
||||
import com.hippo.ehviewer.collectAsState
|
||||
import com.hippo.ehviewer.ui.tools.observed
|
||||
import com.hippo.ehviewer.ui.tools.rememberedAccessor
|
||||
import com.hippo.ehviewer.util.AppConfig
|
||||
import com.hippo.ehviewer.util.Crash
|
||||
import com.hippo.ehviewer.util.ReadableTime
|
||||
@@ -47,13 +51,16 @@ import com.hippo.ehviewer.util.getAppLanguage
|
||||
import com.hippo.ehviewer.util.getLanguages
|
||||
import com.hippo.ehviewer.util.isAtLeastO
|
||||
import com.hippo.ehviewer.util.isAtLeastV
|
||||
import com.hippo.ehviewer.util.sendTo
|
||||
import com.hippo.ehviewer.util.setAppLanguage
|
||||
import com.hippo.files.delete
|
||||
import com.hippo.files.toOkioPath
|
||||
import com.jamal.composeprefs3.ui.prefs.DropDownPref
|
||||
import com.jamal.composeprefs3.ui.prefs.SwitchPref
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import java.io.File
|
||||
import java.util.zip.ZipEntry
|
||||
@@ -62,6 +69,8 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
import okio.Path.Companion.toOkioPath
|
||||
import splitties.init.appCtx
|
||||
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
@@ -91,6 +100,26 @@ fun AdvancedScreen(navigator: DestinationsNavigator) {
|
||||
summary = stringResource(id = R.string.settings_advanced_save_parse_error_body_summary),
|
||||
value = Settings::saveParseErrorBody,
|
||||
)
|
||||
val stripAds = Settings.stripExtraneousAds.asMutableState()
|
||||
SwitchPreference(
|
||||
title = stringResource(id = R.string.settings_strip_extraneous_ads),
|
||||
value = stripAds.rememberedAccessor,
|
||||
)
|
||||
AnimatedVisibility(visible = stripAds.value) {
|
||||
LauncherPreference(
|
||||
title = stringResource(id = R.string.settings_ads_placeholder),
|
||||
contract = ActivityResultContracts.PickVisualMedia(),
|
||||
key = PickVisualMediaRequest(mediaType = ImageOnly),
|
||||
) { uri ->
|
||||
withIOContext {
|
||||
if (uri != null) {
|
||||
uri.toOkioPath() sendTo AdsPlaceholderFile
|
||||
} else {
|
||||
AdsPlaceholderFile.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SwitchPreference(
|
||||
title = stringResource(id = R.string.settings_advanced_save_crash_log),
|
||||
summary = stringResource(id = R.string.settings_advanced_save_crash_log_summary),
|
||||
@@ -273,3 +302,5 @@ fun AdvancedScreen(navigator: DestinationsNavigator) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val AdsPlaceholderFile = appCtx.filesDir.toOkioPath() / "AdsPlaceholder"
|
||||
@@ -74,7 +74,7 @@ suspend fun Context.requestPermission(key: String): Boolean {
|
||||
return awaitActivityResult(ActivityResultContracts.RequestPermission(), key)
|
||||
}
|
||||
|
||||
suspend fun Context.pickVisualMedia(type: VisualMediaType): Uri? = awaitActivityResult(ActivityResultContracts.PickVisualMedia(), PickVisualMediaRequest.Builder().setMediaType(type).build())
|
||||
suspend fun Context.pickVisualMedia(type: VisualMediaType): Uri? = awaitActivityResult(ActivityResultContracts.PickVisualMedia(), PickVisualMediaRequest(mediaType = type))
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
suspend fun Context.requestInstallPermission(): Boolean {
|
||||
|
||||
@@ -26,6 +26,7 @@ open class Page(val index: Int) {
|
||||
LOAD_PAGE,
|
||||
DOWNLOAD_IMAGE,
|
||||
READY,
|
||||
BLOCKED,
|
||||
ERROR,
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import androidx.compose.material.icons.filled.FileCopy
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.filled.Save
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material.icons.filled.Visibility
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -37,6 +38,7 @@ fun ReaderPageSheetMeta(
|
||||
copy: () -> Unit,
|
||||
save: () -> Unit,
|
||||
saveTo: () -> Unit,
|
||||
showAds: (() -> Unit)?,
|
||||
dismiss: () -> Unit,
|
||||
) {
|
||||
@Composable
|
||||
@@ -52,6 +54,7 @@ fun ReaderPageSheetMeta(
|
||||
modifier = Modifier.fillMaxSize() // Workaround for https://issuetracker.google.com/341594885
|
||||
.verticalScroll(rememberScrollState()).navigationBarsPadding(),
|
||||
) {
|
||||
showAds?.let { Item(icon = Icons.Default.Visibility, text = R.string.show_blocked_image, onClick = it) }
|
||||
Item(icon = Icons.Default.Refresh, text = R.string.refresh, onClick = retry)
|
||||
Item(icon = Icons.Default.Refresh, text = R.string.refresh_original, onClick = retryOrigin)
|
||||
Item(icon = Icons.Default.Share, text = R.string.action_share, onClick = share)
|
||||
|
||||
@@ -105,12 +105,6 @@ abstract class PageLoader {
|
||||
|
||||
protected abstract fun onForceRequest(index: Int, orgImg: Boolean)
|
||||
|
||||
fun cancelRequest(index: Int) {
|
||||
onCancelRequest(index)
|
||||
}
|
||||
|
||||
protected abstract fun onCancelRequest(index: Int)
|
||||
|
||||
fun notifyPageWait(index: Int) {
|
||||
pages[index].reset()
|
||||
}
|
||||
@@ -125,7 +119,7 @@ abstract class PageLoader {
|
||||
cache.put(index, image)
|
||||
}
|
||||
pages[index].image = image
|
||||
pages[index].status.value = Page.State.READY
|
||||
pages[index].status.value = if (image.hasQrCode) Page.State.BLOCKED else Page.State.READY
|
||||
}
|
||||
|
||||
fun notifyPageFailed(index: Int, error: String?) {
|
||||
|
||||
@@ -321,6 +321,8 @@
|
||||
<string name="settings_read_volume_page_fast">快</string>
|
||||
<string name="settings_read_volume_page_slow">慢</string>
|
||||
<string name="settings_read_reverse_volume">翻转音量键</string>
|
||||
<string name="settings_strip_extraneous_ads">移除外部广告</string>
|
||||
<string name="settings_ads_placeholder">选择替换外部广告的图片</string>
|
||||
<string name="settings_download">下载</string>
|
||||
<string name="settings_download_download_location">下载路径</string>
|
||||
<string name="settings_download_cant_get_download_location">无法获取下载路径</string>
|
||||
@@ -606,6 +608,8 @@
|
||||
<string name="dark_theme_off">总是关闭</string>
|
||||
<string name="dark_theme_on">总是开启</string>
|
||||
<string name="pref_crop_borders">裁剪边缘</string>
|
||||
<string name="blocked_image">已屏蔽的图片</string>
|
||||
<string name="show_blocked_image">显示已屏蔽的图片</string>
|
||||
<!-- Tachiyomi Reader End-->
|
||||
|
||||
<plurals name="page_count">
|
||||
|
||||
@@ -337,6 +337,8 @@
|
||||
<string name="settings_read_volume_page_fast">Fast</string>
|
||||
<string name="settings_read_volume_page_slow">Slow</string>
|
||||
<string name="settings_read_reverse_volume">Reverse volume key</string>
|
||||
<string name="settings_strip_extraneous_ads">Remove extraneous ads</string>
|
||||
<string name="settings_ads_placeholder">Pick placeholder for removed ads</string>
|
||||
<string name="settings_download">Download</string>
|
||||
<string name="settings_download_download_location">Download location</string>
|
||||
<string name="settings_download_cant_get_download_location">Can\'t get download location</string>
|
||||
@@ -624,6 +626,8 @@
|
||||
<string name="dark_theme_follow_system">Follow system</string>
|
||||
<string name="dark_theme_off">Always off</string>
|
||||
<string name="dark_theme_on">Always on</string>
|
||||
<string name="blocked_image">Blocked image</string>
|
||||
<string name="show_blocked_image">Show blocked image</string>
|
||||
<!-- Tachiyomi Reader End-->
|
||||
|
||||
<plurals name="page_count">
|
||||
|
||||
Generated
+479
-1
@@ -2,6 +2,18 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "adler"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
||||
|
||||
[[package]]
|
||||
name = "android_log-sys"
|
||||
version = "0.3.1"
|
||||
@@ -25,6 +37,55 @@ version = "1.0.87"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10f00e1f6e58a40e807377c75c6a7f97bf9044fab57816f2414e6f5f4499d7b8"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
|
||||
|
||||
[[package]]
|
||||
name = "bardecoder"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "757eeab9757d5ddd88aa1420449f3ce92631339b7f5b3785808da7a148de35d9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"image",
|
||||
"log",
|
||||
"newtype_derive",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit_field"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.7.1"
|
||||
@@ -43,6 +104,12 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "color_quant"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.7"
|
||||
@@ -53,15 +120,58 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d"
|
||||
dependencies = [
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
|
||||
|
||||
[[package]]
|
||||
name = "ehviewer_rust"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"android_logger",
|
||||
"anyhow",
|
||||
"bardecoder",
|
||||
"image",
|
||||
"jni",
|
||||
"jni_fn",
|
||||
"log",
|
||||
"ndk",
|
||||
"quick-xml",
|
||||
"regex-lite",
|
||||
"serde",
|
||||
@@ -69,6 +179,12 @@ dependencies = [
|
||||
"tl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
|
||||
|
||||
[[package]]
|
||||
name = "env_filter"
|
||||
version = "0.1.2"
|
||||
@@ -78,12 +194,116 @@ dependencies = [
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||
|
||||
[[package]]
|
||||
name = "exr"
|
||||
version = "1.72.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "887d93f60543e9a9362ef8a21beedd0a833c5d9610e18c67abe15a5963dcb1a4"
|
||||
dependencies = [
|
||||
"bit_field",
|
||||
"flume",
|
||||
"half 2.4.1",
|
||||
"lebe",
|
||||
"miniz_oxide 0.7.4",
|
||||
"rayon-core",
|
||||
"smallvec",
|
||||
"zune-inflate",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fdeflate"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645"
|
||||
dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.0.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide 0.8.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flume"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181"
|
||||
dependencies = [
|
||||
"spin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gif"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2"
|
||||
dependencies = [
|
||||
"color_quant",
|
||||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "half"
|
||||
version = "1.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403"
|
||||
|
||||
[[package]]
|
||||
name = "half"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crunchy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.24.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder",
|
||||
"color_quant",
|
||||
"exr",
|
||||
"gif",
|
||||
"jpeg-decoder",
|
||||
"num-traits",
|
||||
"png",
|
||||
"qoi",
|
||||
"tiff",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni"
|
||||
version = "0.21.1"
|
||||
@@ -117,6 +337,31 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jpeg-decoder"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0"
|
||||
dependencies = [
|
||||
"rayon",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lebe"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.22"
|
||||
@@ -129,6 +374,110 @@ version = "2.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08"
|
||||
dependencies = [
|
||||
"adler",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"jni-sys",
|
||||
"log",
|
||||
"ndk-sys",
|
||||
"num_enum",
|
||||
"raw-window-handle",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk-sys"
|
||||
version = "0.6.0+11769913"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873"
|
||||
dependencies = [
|
||||
"jni-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "newtype_derive"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac8cd24d9f185bb7223958d8c1ff7a961b74b1953fd05dba7cc568a63b3861ec"
|
||||
dependencies = [
|
||||
"rustc_version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_enum"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179"
|
||||
dependencies = [
|
||||
"num_enum_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_enum_derive"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.17.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"crc32fast",
|
||||
"fdeflate",
|
||||
"flate2",
|
||||
"miniz_oxide 0.7.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b"
|
||||
dependencies = [
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.86"
|
||||
@@ -138,6 +487,15 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "qoi"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.36.1"
|
||||
@@ -156,12 +514,47 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "raw-window-handle"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
|
||||
dependencies = [
|
||||
"either",
|
||||
"rayon-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon-core"
|
||||
version = "1.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
|
||||
dependencies = [
|
||||
"crossbeam-deque",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-lite"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5f5376ea5e30ce23c03eb77cbe4962b988deead10910c372b226388b594c084"
|
||||
dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
@@ -171,6 +564,18 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4f410fedcf71af0345d7607d246e7ad15faaadd49d240ee3b24e5dc21a820ac"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.210"
|
||||
@@ -186,7 +591,7 @@ version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5"
|
||||
dependencies = [
|
||||
"half",
|
||||
"half 1.8.3",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@@ -201,6 +606,27 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.77"
|
||||
@@ -232,12 +658,40 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiff"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"jpeg-decoder",
|
||||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tl"
|
||||
version = "0.7.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b130bd8a58c163224b44e217b4239ca7b927d82bf6cc2fea1fc561d15056e3f7"
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"toml_datetime",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.12"
|
||||
@@ -254,6 +708,12 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "weezl"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.9"
|
||||
@@ -401,3 +861,21 @@ name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.6.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zune-inflate"
|
||||
version = "0.2.54"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02"
|
||||
dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
@@ -7,7 +7,7 @@ edition = "2021"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
regex-lite = { version = "0.1" }
|
||||
regex-lite = "0.1"
|
||||
jni = "0.21"
|
||||
jni_fn = "0.1"
|
||||
quick-xml = { default-features = false, version = "0.36" }
|
||||
@@ -16,7 +16,10 @@ log = "0.4"
|
||||
android_logger = { default-features = false, version = "0.14" }
|
||||
serde_cbor = "0.11"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
anyhow = { version = "1.0" }
|
||||
anyhow = "1.0"
|
||||
bardecoder = "0.5"
|
||||
image = "0.24"
|
||||
ndk = { version = "0.9", features = ["bitmap"] }
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
|
||||
@@ -2,13 +2,21 @@ mod parser;
|
||||
|
||||
use android_logger::Config;
|
||||
use anyhow::{anyhow, ensure, Result};
|
||||
use jni::objects::JByteBuffer;
|
||||
use jni::sys::{jint, jobject, JavaVM, JNI_VERSION_1_6};
|
||||
use bardecoder::decode::Decode;
|
||||
use bardecoder::default_builder;
|
||||
use bardecoder::extract::QRExtractor;
|
||||
use bardecoder::util::qr::{QRData, QRError};
|
||||
use image::ImageBuffer;
|
||||
use jni::objects::{JByteBuffer, JClass};
|
||||
use jni::sys::{jboolean, jint, jobject, JavaVM, JNI_VERSION_1_6};
|
||||
use jni::JNIEnv;
|
||||
use jni_fn::jni_fn;
|
||||
use log::LevelFilter;
|
||||
use ndk::bitmap::Bitmap;
|
||||
use serde::Serialize;
|
||||
use std::ffi::c_void;
|
||||
use std::io::Cursor;
|
||||
use std::ptr::slice_from_raw_parts;
|
||||
use std::ptr::slice_from_raw_parts_mut;
|
||||
use std::str::from_utf8_unchecked;
|
||||
use tl::ParserOptions;
|
||||
@@ -64,6 +72,12 @@ trait ThrowingHasDefault {
|
||||
fn default() -> Self;
|
||||
}
|
||||
|
||||
impl ThrowingHasDefault for jboolean {
|
||||
fn default() -> Self {
|
||||
0 as jboolean
|
||||
}
|
||||
}
|
||||
|
||||
impl ThrowingHasDefault for jobject {
|
||||
fn default() -> Self {
|
||||
0 as jobject
|
||||
@@ -133,6 +147,39 @@ fn query_childs_first_match_attr<'a>(
|
||||
get_node_handle_attr(&iter.next()?, parser, attr)
|
||||
}
|
||||
|
||||
fn get_bitmap_handle(env: &mut JNIEnv, bitmap: jobject) -> Bitmap {
|
||||
unsafe { Bitmap::from_jni(env.get_raw(), bitmap) }
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
struct Nothing {}
|
||||
|
||||
impl Decode<QRData, String, QRError> for Nothing {
|
||||
fn decode(&self, result: Result<QRData, QRError>) -> Result<String, QRError> {
|
||||
result.map(|_| String::new())
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[allow(non_snake_case)]
|
||||
#[jni_fn("com.hippo.ehviewer.image.ImageKt")]
|
||||
pub fn hasQrCode(mut env: JNIEnv, _class: JClass, object: jobject) -> jboolean {
|
||||
let handle = get_bitmap_handle(&mut env, object);
|
||||
jni_throwing(&mut env, |_| {
|
||||
let bitmap_info = handle.info()?;
|
||||
let (width, height) = (bitmap_info.width(), bitmap_info.height());
|
||||
let ptr = handle.lock_pixels()? as *const u8;
|
||||
let buffer = unsafe { &*slice_from_raw_parts(ptr, (width * height * 4) as usize) };
|
||||
let mut decoder = default_builder();
|
||||
decoder.qr(Box::new(QRExtractor {}), Box::new(Nothing {}));
|
||||
let image = ImageBuffer::from_raw(width, height, buffer);
|
||||
let result = image.map(|img| decoder.build().decode(&img));
|
||||
handle.unlock_pixels()?;
|
||||
let vec = result.ok_or(anyhow!("Internal Error!"))?;
|
||||
Ok(vec.iter().any(|i| i.is_ok()) as jboolean)
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn JNI_OnLoad(_: JavaVM, _: *mut c_void) -> jint {
|
||||
android_logger::init_once(Config::default().with_max_level(LevelFilter::Debug));
|
||||
|
||||
Reference in new issue
Block a user