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:
authored and GitHub committed 2024-09-13 16:10:05 +00:00
1 parent 42ed51094e
commit 0d55f319c9
26 files changed
+730 -79

No files matched your search

+1 -1
View File
@@ -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">
+4
View File
@@ -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">
+479 -1
View File
@@ -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",
]
+5 -2
View File
@@ -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"
+49 -2
View File
@@ -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));