Compare commits

...
41 Commits
Author SHA1 Message Date
tarsin 25d4d5e393 Bump to 1.8.8.0
Releases / Build (push) Has been cancelled
2023-05-25 15:33:07 +08:00
tarsin 1743caa869 Remove unused padding 2023-05-25 15:13:27 +08:00
tarsin 958fdb19b4 Revert "Decode cf protected email, step 1"
This reverts commit d44981a70c.
2023-05-25 15:02:14 +08:00
tarsin 4b63648a79 Revert "Decode cf protected email, step 2, Resolve #1102"
This reverts commit 74c6bfa479.
2023-05-25 15:02:00 +08:00
tarsin 498b84df37 Revert "Optimize code"
This reverts commit 6ed3fb29d9.
2023-05-25 15:01:49 +08:00
tarsin 7a4e65259d Revert "Uniform html parse behaviour"
This reverts commit c5c1170383.
2023-05-25 15:01:38 +08:00
tarsin 8cb8dcbe0f Resolve #1067 2023-05-25 14:16:13 +08:00
tarsin da37b3cdff Upgrade compose bom 2023-05-25 13:44:10 +08:00
renovate[bot] 0a1768e481 Update dependency androidx.core:core-ktx to v1.11.0-beta01 2023-05-25 13:41:57 +08:00
renovate[bot] 818d16a32d Update dependency androidx.paging:paging-common-ktx to v3.2.0-alpha06 2023-05-25 13:41:03 +08:00
renovate[bot] e9a5d1cc7c Update dependency androidx.paging:paging-compose to v1.0.0-alpha20 2023-05-25 13:40:19 +08:00
renovate[bot] dac56f3e32 Update dependency androidx.constraintlayout:constraintlayout-compose to v1.1.0-alpha10 2023-05-25 03:55:42 +00:00
renovate[bot] d6cbcdc6cd Update dependency androidx.activity:activity-compose to v1.7.2 2023-05-25 01:29:39 +00:00
tarsin c5c1170383 Uniform html parse behaviour 2023-05-24 20:12:24 +08:00
tarsin 6ed3fb29d9 Optimize code 2023-05-24 19:28:55 +08:00
tarsin 74c6bfa479 Decode cf protected email, step 2, Resolve #1102 2023-05-24 19:19:47 +08:00
tarsin d44981a70c Decode cf protected email, step 1 2023-05-24 18:51:22 +08:00
tarsin 06e411d332 Fixup! Rewrite LauncherUtils 2023-05-23 13:44:32 +08:00
tarsin f419189749 Rewrite LauncherUtils 2023-05-23 13:11:57 +08:00
tarsin aa1e2102fc Fix deprecated coil API 2023-05-22 21:53:59 +08:00
tarsin 306ce9ed21 Always use ImageDecoderDecoder for better performance 2023-05-22 12:33:12 +08:00
renovate[bot] 544fcb2e88 Update dependency io.coil-kt:coil-compose to v2.4.0 2023-05-21 21:44:29 +00:00
tarsin cb01df4071 Revert "Update dependency dev.chrisbanes.compose:compose-bom to v2023.04.00-SNAPSHOT"
This reverts commit 38310b7c5a.
2023-05-21 10:42:08 +08:00
renovate[bot] 38310b7c5a Update dependency dev.chrisbanes.compose:compose-bom to v2023.04.00-SNAPSHOT 2023-05-20 19:21:10 +00:00
tarsin fcd56ff4e9 Use compose-bom 2023.04.00-alpha04 for reproducible artifact 2023-05-21 00:16:58 +08:00
tarsin 41300a9fd4 Fix okhttp dependency 2023-05-21 00:12:28 +08:00
tarsin 0bd5f9dc31 Downgrade ArrowKt to stable release for reproducible artifacts 2023-05-21 00:11:10 +08:00
tarsin 004ecd0dd3 Resolve #1095 2023-05-21 00:06:56 +08:00
tarsin fc24a18ac5 Bump to 1.8.8.0-beta
Releases / Build (push) Has been cancelled
2023-05-19 19:19:43 +08:00
tarsin a9a8c68b94 Fix code style, fuck Android Studio 2023-05-19 18:55:18 +08:00
tarsin 193d2f8d9e Fix rememberMemorized implementation 2023-05-19 18:50:17 +08:00
tarsin 538da9b98f Cleanup rememberSaveable usage 2023-05-19 17:25:29 +08:00
tarsin 071b597552 Introduce rememberMemorized 2023-05-19 17:06:32 +08:00
tarsin 8926d74378 Introduce rememberGlobally 2023-05-19 13:10:15 +08:00
tarsin 39a4b03bc3 Avoid TransactionTooLargeException 2023-05-19 12:16:07 +08:00
tarsin 9202bbc7dc Workaround #1092 2023-05-19 11:43:21 +08:00
tarsin f5601ebca1 Don't saveInstanceState when data too large, Workaround #1092 2023-05-19 10:50:18 +08:00
tarsin 2f978b554e Adjust kotlin compiler options 2023-05-18 13:55:06 +08:00
tarsin f48a90d941 Upgrade r8 2023-05-18 13:29:41 +08:00
tarsin 4f0d9fa83c Allow downsampling on LargePreviews 2023-05-18 11:52:25 +08:00
tarsin 57a747ed9c Fix cycled preview when PreviewPages == 1 2023-05-17 23:39:19 +08:00
21 changed files with 242 additions and 137 deletions

No files matched your search

+17 -13
View File
@@ -57,8 +57,8 @@ android {
applicationId = "moe.tarsin.ehviewer" applicationId = "moe.tarsin.ehviewer"
minSdk = 28 minSdk = 28
targetSdk = 33 targetSdk = 33
versionCode = 180038 versionCode = 180039
versionName = "1.8.8.0-alpha" versionName = "1.8.8.0"
resourceConfigurations.addAll( resourceConfigurations.addAll(
listOf( listOf(
"zh", "zh",
@@ -95,6 +95,8 @@ android {
freeCompilerArgs = listOf( freeCompilerArgs = listOf(
// https://kotlinlang.org/docs/compiler-reference.html#progressive // https://kotlinlang.org/docs/compiler-reference.html#progressive
"-progressive", "-progressive",
"-Xjvm-default=all",
"-Xlambdas=indy",
"-opt-in=coil.annotation.ExperimentalCoilApi", "-opt-in=coil.annotation.ExperimentalCoilApi",
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi", "-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
@@ -156,22 +158,22 @@ android {
dependencies { dependencies {
// https://developer.android.com/jetpack/androidx/releases/activity // https://developer.android.com/jetpack/androidx/releases/activity
implementation("androidx.activity:activity-compose:1.7.1") implementation("androidx.activity:activity-compose:1.7.2")
implementation("androidx.appcompat:appcompat:1.6.1") implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha05") implementation("androidx.biometric:biometric-ktx:1.2.0-alpha05")
implementation("androidx.browser:browser:1.5.0") implementation("androidx.browser:browser:1.5.0")
implementation("androidx.collection:collection-ktx:1.3.0-alpha04") implementation("androidx.collection:collection-ktx:1.3.0-alpha04")
// https://developer.android.com/jetpack/androidx/releases/compose-material3 // https://developer.android.com/jetpack/androidx/releases/compose-material3
// api(platform("androidx.compose:compose-bom:2023.03.00")) // api(platform("androidx.compose:compose-bom:2023.05.00"))
api(platform("dev.chrisbanes.compose:compose-bom:2023.04.00-SNAPSHOT")) api(platform("dev.chrisbanes.compose:compose-bom:2023.04.00-beta01"))
implementation("androidx.compose.material:material-icons-extended") implementation("androidx.compose.material:material-icons-extended")
implementation("androidx.compose.material3:material3") implementation("androidx.compose.material3:material3")
implementation("androidx.compose.material3:material3-window-size-class") implementation("androidx.compose.material3:material3-window-size-class")
implementation("androidx.core:core-ktx:1.11.0-alpha04") implementation("androidx.core:core-ktx:1.11.0-beta01")
implementation("androidx.constraintlayout:constraintlayout-compose:1.1.0-alpha09") implementation("androidx.constraintlayout:constraintlayout-compose:1.1.0-alpha10")
implementation("androidx.coordinatorlayout:coordinatorlayout:1.2.0") implementation("androidx.coordinatorlayout:coordinatorlayout:1.2.0")
implementation("androidx.drawerlayout:drawerlayout:1.2.0") implementation("androidx.drawerlayout:drawerlayout:1.2.0")
// https://developer.android.com/jetpack/androidx/releases/lifecycle // https://developer.android.com/jetpack/androidx/releases/lifecycle
@@ -184,8 +186,8 @@ dependencies {
implementation("androidx.navigation:navigation-compose:$nav_version") implementation("androidx.navigation:navigation-compose:$nav_version")
// https://developer.android.com/jetpack/androidx/releases/paging // https://developer.android.com/jetpack/androidx/releases/paging
implementation("androidx.paging:paging-common-ktx:3.2.0-alpha05") implementation("androidx.paging:paging-common-ktx:3.2.0-alpha06")
implementation("androidx.paging:paging-compose:1.0.0-alpha19") implementation("androidx.paging:paging-compose:1.0.0-alpha20")
implementation("androidx.preference:preference-ktx:1.2.0") implementation("androidx.preference:preference-ktx:1.2.0")
implementation("androidx.recyclerview:recyclerview:1.3.0") implementation("androidx.recyclerview:recyclerview:1.3.0")
@@ -205,7 +207,7 @@ dependencies {
implementation("com.google.android.material:material:1.9.0") implementation("com.google.android.material:material:1.9.0")
// https://square.github.io/okhttp/changelogs/changelog/ // https://square.github.io/okhttp/changelogs/changelog/
implementation("com.squareup.okhttp3:okhttp-bom:5.0.0-alpha.11") implementation(platform("com.squareup.okhttp3:okhttp-bom:5.0.0-alpha.11"))
implementation("com.squareup.okhttp3:okhttp-coroutines") implementation("com.squareup.okhttp3:okhttp-coroutines")
implementation("com.squareup.okio:okio-jvm:3.3.0") implementation("com.squareup.okio:okio-jvm:3.3.0")
@@ -217,12 +219,14 @@ dependencies {
implementation("dev.rikka.rikkax.preference:simplemenu-preference:1.0.3") implementation("dev.rikka.rikkax.preference:simplemenu-preference:1.0.3")
implementation("dev.rikka.rikkax.material:material-preference:2.0.0") implementation("dev.rikka.rikkax.material:material-preference:2.0.0")
implementation(platform("io.arrow-kt:arrow-stack:2.0.0-SNAPSHOT")) implementation(platform("io.arrow-kt:arrow-stack:1.2.0-RC"))
implementation("io.arrow-kt:arrow-functions")
implementation("io.arrow-kt:arrow-fx-coroutines") implementation("io.arrow-kt:arrow-fx-coroutines")
// https://coil-kt.github.io/coil/changelog/ // https://coil-kt.github.io/coil/changelog/
implementation("io.coil-kt:coil-compose:2.3.0") implementation(platform("io.coil-kt:coil-bom:2.4.0"))
implementation("io.coil-kt:coil-compose")
implementation("io.coil-kt:coil-gif")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-cbor:1.5.1") implementation("org.jetbrains.kotlinx:kotlinx-serialization-cbor:1.5.1")
implementation("org.jsoup:jsoup:1.16.1") implementation("org.jsoup:jsoup:1.16.1")
@@ -25,6 +25,7 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import coil.ImageLoader import coil.ImageLoader
import coil.ImageLoaderFactory import coil.ImageLoaderFactory
import coil.decode.ImageDecoderDecoder
import coil.util.DebugLogger import coil.util.DebugLogger
import com.hippo.ehviewer.client.EhCookieStore import com.hippo.ehviewer.client.EhCookieStore
import com.hippo.ehviewer.client.EhDns import com.hippo.ehviewer.client.EhDns
@@ -137,7 +138,10 @@ class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactor
override fun newImageLoader(): ImageLoader { override fun newImageLoader(): ImageLoader {
return ImageLoader.Builder(this).apply { return ImageLoader.Builder(this).apply {
okHttpClient(nonCacheOkHttpClient) okHttpClient(nonCacheOkHttpClient)
components { add(MergeInterceptor) } components {
add { result, options, _ -> ImageDecoderDecoder(result.source, options, false) }
add(MergeInterceptor)
}
diskCache(imageCache) diskCache(imageCache)
crossfade(300) crossfade(300)
error(R.drawable.image_failed) error(R.drawable.image_failed)
@@ -23,6 +23,8 @@ import java.net.Socket
import javax.net.ssl.SSLSocket import javax.net.ssl.SSLSocket
import javax.net.ssl.SSLSocketFactory import javax.net.ssl.SSLSocketFactory
private const val EXCEPTIONAL_DOMAIN = "hath.network"
object EhSSLSocketFactory : SSLSocketFactory() { object EhSSLSocketFactory : SSLSocketFactory() {
private val sslSocketFactory = getDefault() as SSLSocketFactory private val sslSocketFactory = getDefault() as SSLSocketFactory
override fun getDefaultCipherSuites(): Array<String> { override fun getDefaultCipherSuites(): Array<String> {
@@ -34,7 +36,7 @@ object EhSSLSocketFactory : SSLSocketFactory() {
} }
override fun createSocket(s: Socket, host: String, port: Int, autoClose: Boolean): Socket { override fun createSocket(s: Socket, host: String, port: Int, autoClose: Boolean): Socket {
val address = s.inetAddress.hostAddress.takeIf { EhDns.isInHosts(host) } val address = s.inetAddress.hostAddress.takeIf { EhDns.isInHosts(host) || EXCEPTIONAL_DOMAIN in host }
val socket = sslSocketFactory.createSocket(s, address ?: host, port, autoClose) as SSLSocket val socket = sslSocketFactory.createSocket(s, address ?: host, port, autoClose) as SSLSocket
val sslSession = socket.session val sslSession = socket.session
Log.d( Log.d(
@@ -1,21 +0,0 @@
package com.hippo.ehviewer.coil
import coil.disk.DiskCache
inline fun DiskCache.edit(key: String, block: DiskCache.Editor.() -> Unit): Boolean {
val editor = edit(key) ?: return false
editor.runCatching {
block(this)
}.onFailure {
editor.abort()
throw it
}.onSuccess {
editor.commit()
}
return true
}
inline fun DiskCache.read(key: String, block: DiskCache.Snapshot.() -> Unit): Boolean {
(get(key) ?: return false).use { block(it) }
return true
}
@@ -0,0 +1,18 @@
package com.hippo.ehviewer.coil
import coil.disk.DiskCache
suspend inline fun DiskCache.edit(key: String, crossinline block: suspend DiskCache.Editor.() -> Unit): Boolean {
val editor = openEditor(key) ?: return false
editor.runCatching {
block(this)
}.onFailure {
editor.abort()
throw it
}.onSuccess {
editor.commit()
}
return true
}
inline fun <T> DiskCache.read(key: String, block: DiskCache.Snapshot.() -> T): T? = openSnapshot(key)?.use(block)
@@ -11,6 +11,7 @@ import coil.request.ImageRequest
import coil.size.Size import coil.size.Size
import com.hippo.ehviewer.client.data.GalleryInfo import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.data.GalleryPreview import com.hippo.ehviewer.client.data.GalleryPreview
import com.hippo.ehviewer.client.data.NormalGalleryPreview
import com.hippo.ehviewer.client.thumbUrl import com.hippo.ehviewer.client.thumbUrl
import com.hippo.ehviewer.client.url import com.hippo.ehviewer.client.url
@@ -19,14 +20,13 @@ fun ImageRequest.Builder.ehUrl(info: GalleryInfo) = apply {
data(info.thumbUrl) data(info.thumbUrl)
memoryCacheKey(key) memoryCacheKey(key)
diskCacheKey(key) diskCacheKey(key)
size(Size.ORIGINAL)
} }
fun ImageRequest.Builder.ehPreview(preview: GalleryPreview) = apply { fun ImageRequest.Builder.ehPreview(preview: GalleryPreview) = apply {
data(preview.url) data(preview.url)
memoryCacheKey(preview.imageKey) memoryCacheKey(preview.imageKey)
diskCacheKey(preview.imageKey) diskCacheKey(preview.imageKey)
size(Size.ORIGINAL) if (preview is NormalGalleryPreview) size(Size.ORIGINAL)
} }
private val stubResult = DecodeResult(ColorDrawable(Color.BLACK), false) private val stubResult = DecodeResult(ColorDrawable(Color.BLACK), false)
@@ -64,7 +64,7 @@ class SpiderDen(private val mGalleryInfo: GalleryInfo) {
private fun containInCache(index: Int): Boolean { private fun containInCache(index: Int): Boolean {
val key = getImageKey(mGid, index) val key = getImageKey(mGid, index)
return sCache[key]?.use { true } ?: false return sCache.read(key) { true } ?: false
} }
private fun containInDownloadDir(index: Int): Boolean { private fun containInDownloadDir(index: Int): Boolean {
@@ -84,11 +84,11 @@ class SpiderDen(private val mGalleryInfo: GalleryInfo) {
it sendTo outFd it sendTo outFd
} }
} }
} true
}.getOrElse { } ?: false
}.onFailure {
it.printStackTrace() it.printStackTrace()
false }.getOrDefault(false)
}
} }
operator fun contains(index: Int): Boolean { operator fun contains(index: Int): Boolean {
@@ -199,12 +199,11 @@ class SpiderDen(private val mGalleryInfo: GalleryInfo) {
val key = getImageKey(mGid, index) val key = getImageKey(mGid, index)
// Read from diskCache first // Read from diskCache first
sCache[key]?.use { snapshot -> sCache.read(key) {
runCatching { runCatching {
UniFile.fromFile(snapshot.data.toFile())!!.openFileDescriptor("r").use { UniFile.fromFile(data.toFile())!!.openFileDescriptor("r").use {
it sendTo toFd it sendTo toFd
} }
}.onSuccess {
return true return true
}.onFailure { }.onFailure {
it.printStackTrace() it.printStackTrace()
@@ -235,15 +234,14 @@ class SpiderDen(private val mGalleryInfo: GalleryInfo) {
fun getExtension(index: Int): String? { fun getExtension(index: Int): String? {
val key = getImageKey(mGid, index) val key = getImageKey(mGid, index)
return sCache[key]?.use { it.metadata.toNioPath().readText() } return sCache.read(key) { metadata.toNioPath().readText() }
?: downloadDir?.let { findImageFile(it, index) } ?: downloadDir?.let { findImageFile(it, index) }?.name.let { FileUtils.getExtensionFromFilename(it) }
?.name.let { FileUtils.getExtensionFromFilename(it) }
} }
fun getImageSource(index: Int): CloseableSource? { fun getImageSource(index: Int): CloseableSource? {
if (mode == SpiderQueen.MODE_READ) { if (mode == SpiderQueen.MODE_READ) {
val key = getImageKey(mGid, index) val key = getImageKey(mGid, index)
val snapshot = sCache[key] val snapshot = sCache.openSnapshot(key)
if (snapshot != null) { if (snapshot != null) {
val source = ImageDecoder.createSource(snapshot.data.toFile()) val source = ImageDecoder.createSource(snapshot.data.toFile())
return object : CloseableSource, AutoCloseable by snapshot { return object : CloseableSource, AutoCloseable by snapshot {
@@ -2,6 +2,8 @@ package com.hippo.ehviewer.spider
import coil.disk.DiskCache import coil.disk.DiskCache
import com.hippo.ehviewer.EhApplication import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.coil.edit
import com.hippo.ehviewer.coil.read
import com.hippo.ehviewer.legacy.readLegacySpiderInfo import com.hippo.ehviewer.legacy.readLegacySpiderInfo
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import com.hippo.unifile.openInputStream import com.hippo.unifile.openInputStream
@@ -10,6 +12,7 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.cbor.Cbor import kotlinx.serialization.cbor.Cbor
import kotlinx.serialization.decodeFromByteArray import kotlinx.serialization.decodeFromByteArray
import kotlinx.serialization.encodeToByteArray import kotlinx.serialization.encodeToByteArray
import moe.tarsin.coroutines.runSuspendCatching
import java.io.File import java.io.File
@Serializable @Serializable
@@ -35,15 +38,13 @@ fun SpiderInfo.write(file: UniFile) {
} }
} }
fun SpiderInfo.saveToCache() { suspend fun SpiderInfo.saveToCache() {
val entry = spiderInfoCache.edit(gid.toString()) ?: return runSuspendCatching {
runCatching { spiderInfoCache.edit(gid.toString()) {
entry.data.toFile().writeBytes(Cbor.encodeToByteArray(this)) data.toFile().writeBytes(Cbor.encodeToByteArray(this))
}
}.onFailure { }.onFailure {
it.printStackTrace() it.printStackTrace()
entry.abort()
}.onSuccess {
entry.commit()
} }
} }
@@ -54,14 +55,13 @@ private val spiderInfoCache by lazy {
} }
fun readFromCache(gid: Long): SpiderInfo? { fun readFromCache(gid: Long): SpiderInfo? {
val snapshot = spiderInfoCache[gid.toString()] ?: return null return spiderInfoCache.read(gid.toString()) {
return runCatching { runCatching {
snapshot.use { Cbor.decodeFromByteArray<SpiderInfo>(data.toFile().readBytes())
return Cbor.decodeFromByteArray(it.data.toFile().readBytes()) }.onFailure {
} it.printStackTrace()
}.onFailure { }.getOrNull()
it.printStackTrace() }
}.getOrNull()
} }
fun readCompatFromUniFile(file: UniFile): SpiderInfo? { fun readCompatFromUniFile(file: UniFile): SpiderInfo? {
@@ -49,6 +49,7 @@ import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
@@ -335,8 +336,7 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
it.gid == galleryInfo.gid && it.token == galleryInfo.token it.gid == galleryInfo.gid && it.token == galleryInfo.token
} }
} }
} } ?: readFromCache(galleryInfo.gid)?.takeIf { it.gid == galleryInfo.gid && it.token == galleryInfo.token }
?: readFromCache(galleryInfo.gid)?.takeIf { it.gid == galleryInfo.gid && it.token == galleryInfo.token }
} }
@Throws(ParseException::class) @Throws(ParseException::class)
@@ -427,7 +427,7 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
private fun writeSpiderInfoToLocal() { private fun writeSpiderInfoToLocal() {
if (!isReady) return if (!isReady) return
mSpiderDen.downloadDir?.run { createFile(SPIDER_INFO_FILENAME).also { mSpiderInfo.write(it) } } mSpiderDen.downloadDir?.run { createFile(SPIDER_INFO_FILENAME).also { mSpiderInfo.write(it) } }
mSpiderInfo.saveToCache() runBlocking { mSpiderInfo.saveToCache() }
} }
private fun isStateDone(state: Int): Boolean { private fun isStateDone(state: Int): Boolean {
@@ -18,9 +18,12 @@ package com.hippo.ehviewer.ui
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheet
@@ -103,7 +106,10 @@ fun GalleryInfoBottomSheet(
} }
} }
ModalBottomSheet(onDismissRequest) { ModalBottomSheet(
onDismissRequest = onDismissRequest,
windowInsets = WindowInsets(0),
) {
check(data.size == keys.size) check(data.size == keys.size)
val context = LocalContext.current val context = LocalContext.current
val navController = LocalNavController.current val navController = LocalNavController.current
@@ -114,23 +120,25 @@ fun GalleryInfoBottomSheet(
Text(text = stringResource(id = R.string.gallery_info), style = MaterialTheme.typography.titleLarge) Text(text = stringResource(id = R.string.gallery_info), style = MaterialTheme.typography.titleLarge)
} }
CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.labelLarge) { CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.labelLarge) {
keys.forEachIndexed { index, i -> LazyColumn {
val key = stringResource(i) itemsIndexed(keys) { index, i ->
Row( val key = stringResource(i)
modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.keyline_margin)).clickable { Row(
if (index == INDEX_PARENT) { modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.keyline_margin)).clickable {
data[index]?.let { navController.navWithUrl(it) } if (index == INDEX_PARENT) {
} else { data[index]?.let { navController.navWithUrl(it) }
context tellClipboardWithToast data[index] } else {
if (index == INDEX_URL) { context tellClipboardWithToast data[index]
// Save it to avoid detect the gallery if (index == INDEX_URL) {
Settings.putClipboardTextHashCode(data[index].hashCode()) // Save it to avoid detect the gallery
Settings.putClipboardTextHashCode(data[index].hashCode())
}
} }
} }.fillMaxWidth(),
}.fillMaxWidth(), ) {
) { Text(key, modifier = Modifier.width(90.dp).padding(8.dp))
Text(key, modifier = Modifier.width(90.dp).padding(8.dp)) Text(data[index].orEmpty(), modifier = Modifier.padding(8.dp))
Text(data[index].orEmpty(), modifier = Modifier.padding(8.dp)) }
} }
} }
} }
@@ -73,7 +73,6 @@ import com.hippo.ehviewer.util.addTextToClipboard
import com.hippo.ehviewer.util.getClipboardManager import com.hippo.ehviewer.util.getClipboardManager
import com.hippo.ehviewer.util.getParcelableExtraCompat import com.hippo.ehviewer.util.getParcelableExtraCompat
import com.hippo.ehviewer.util.getUrlFromClipboard import com.hippo.ehviewer.util.getUrlFromClipboard
import com.hippo.ehviewer.util.initLauncher
import com.hippo.ehviewer.yorozuya.IOUtils import com.hippo.ehviewer.yorozuya.IOUtils
import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.util.lang.launchUI import eu.kanade.tachiyomi.util.lang.launchUI
@@ -91,10 +90,6 @@ class MainActivity : EhActivity() {
private val connectivityManager by lazy { getSystemService<ConnectivityManager>()!! } private val connectivityManager by lazy { getSystemService<ConnectivityManager>()!! }
private val availableNetworks = mutableListOf<Network>() private val availableNetworks = mutableListOf<Network>()
init {
initLauncher()
}
private fun saveImageToTempFile(uri: Uri): File? { private fun saveImageToTempFile(uri: Uri): File? {
val src = ImageDecoder.createSource(contentResolver, uri) val src = ImageDecoder.createSource(contentResolver, uri)
val bitmap = runCatching { val bitmap = runCatching {
@@ -0,0 +1,56 @@
package com.hippo.ehviewer.ui.compose
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.saveable.LocalSaveableStateRegistry
private val remembered = hashMapOf<String, Any>()
// This works mostly like [rememberSaveable], but saves data in global hashmap, to avoid [TransactionTooLargeException]
@Composable
fun <T : Any> rememberMemorized(
vararg inputs: Any?,
key: String? = null,
init: () -> T,
): T {
// key is the one provided by the user or the one generated by the compose runtime
val finalKey = if (!key.isNullOrEmpty()) {
key
} else {
// currentCompositeKeyHash is unreliable here
// Is this Composable inlined or its groups elided?
// Or a compose compiler bug?
init.javaClass.toString()
}
val registry = LocalSaveableStateRegistry.current
// value is restored using the registry or created via [init] lambda
val value = remember(*inputs) {
@Suppress("UNCHECKED_CAST")
val restored = registry?.consumeRestored(finalKey)?.let {
remembered.remove(it as String)
} as T?
restored ?: init()
}
// re-register if the registry or key has been changed
if (registry != null) {
// we want to use the latest instances of saver and value in the valueProvider lambda
// without restarting DisposableEffect as it would cause re-registering the provider in
// the different order. so we use rememberUpdatedState.
val valueState = rememberUpdatedState(value)
DisposableEffect(registry, finalKey) {
val valueProvider = {
finalKey.also { remembered[it] = valueState.value }
}
val entry = registry.registerProvider(finalKey, valueProvider)
onDispose {
entry.unregister()
}
}
}
return value
}
@@ -28,6 +28,7 @@ import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
import com.hippo.ehviewer.R import com.hippo.ehviewer.R
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.databinding.WidgetContentLayoutBinding import com.hippo.ehviewer.databinding.WidgetContentLayoutBinding
import com.hippo.ehviewer.ui.legacy.ViewTransition.OnShowViewListener import com.hippo.ehviewer.ui.legacy.ViewTransition.OnShowViewListener
import com.hippo.ehviewer.ui.legacy.easyrecyclerview.HandlerDrawable import com.hippo.ehviewer.ui.legacy.easyrecyclerview.HandlerDrawable
@@ -41,7 +42,7 @@ import com.hippo.ehviewer.yorozuya.collect.IntList
import rikka.core.res.resolveColor import rikka.core.res.resolveColor
class ContentLayout(context: Context, attrs: AttributeSet? = null) : FrameLayout(context, attrs) { class ContentLayout(context: Context, attrs: AttributeSet? = null) : FrameLayout(context, attrs) {
private lateinit var mContentHelper: ContentHelper<*> private lateinit var mContentHelper: ContentHelper
private val mRecyclerViewOriginBottom: Int private val mRecyclerViewOriginBottom: Int
private val mFastScrollerOriginBottom: Int private val mFastScrollerOriginBottom: Int
private val binding: WidgetContentLayoutBinding private val binding: WidgetContentLayoutBinding
@@ -64,7 +65,7 @@ class ContentLayout(context: Context, attrs: AttributeSet? = null) : FrameLayout
val fastScroller val fastScroller
get() = binding.fastScroller get() = binding.fastScroller
fun setHelper(helper: ContentHelper<*>) { fun setHelper(helper: ContentHelper) {
mContentHelper = helper mContentHelper = helper
helper.init(this) helper.init(this)
} }
@@ -128,7 +129,7 @@ class ContentLayout(context: Context, attrs: AttributeSet? = null) : FrameLayout
super.onRestoreInstanceState(mContentHelper.restoreInstanceState(state)) super.onRestoreInstanceState(mContentHelper.restoreInstanceState(state))
} }
abstract class ContentHelper<E : Parcelable> : OnShowViewListener { abstract class ContentHelper : OnShowViewListener {
/** /**
* Generate task id * Generate task id
*/ */
@@ -143,7 +144,7 @@ class ContentLayout(context: Context, attrs: AttributeSet? = null) : FrameLayout
/** /**
* Store data * Store data
*/ */
private var mData = ArrayList<E>() private var mData = ArrayList<GalleryInfo>()
/** /**
* Store the page divider index * Store the page divider index
@@ -287,10 +288,10 @@ class ContentLayout(context: Context, attrs: AttributeSet? = null) : FrameLayout
mEmptyString = str mEmptyString = str
} }
val data: List<E> val data: List<GalleryInfo>
get() = mData get() = mData
fun getDataAtEx(location: Int): E? { fun getDataAtEx(location: Int): GalleryInfo? {
return if (location >= 0 && location < mData.size) { return if (location >= 0 && location < mData.size) {
mData[location] mData[location]
} else { } else {
@@ -298,7 +299,7 @@ class ContentLayout(context: Context, attrs: AttributeSet? = null) : FrameLayout
} }
} }
val firstVisibleItem: E? val firstVisibleItem: GalleryInfo?
get() = getDataAtEx(LayoutManagerUtils.getFirstVisibleItemPosition(binding.recyclerView.layoutManager)) get() = getDataAtEx(LayoutManagerUtils.getFirstVisibleItemPosition(binding.recyclerView.layoutManager))
fun size(): Int { fun size(): Int {
@@ -309,8 +310,8 @@ class ContentLayout(context: Context, attrs: AttributeSet? = null) : FrameLayout
return mCurrentTaskId == taskId return mCurrentTaskId == taskId
} }
protected abstract fun isDuplicate(d1: E, d2: E): Boolean protected abstract fun isDuplicate(d1: GalleryInfo, d2: GalleryInfo): Boolean
private fun removeDuplicateData(data: List<E>, start: Int, end: Int) { private fun removeDuplicateData(data: List<GalleryInfo>, start: Int, end: Int) {
val slicedData = mData.slice(start.coerceAtLeast(0) until end.coerceAtMost(mData.size)) val slicedData = mData.slice(start.coerceAtLeast(0) until end.coerceAtMost(mData.size))
data.dropWhile { d1 -> data.dropWhile { d1 ->
slicedData.any { d2 -> slicedData.any { d2 ->
@@ -319,8 +320,8 @@ class ContentLayout(context: Context, attrs: AttributeSet? = null) : FrameLayout
} }
} }
protected open fun onAddData(data: List<E>) {} protected open fun onAddData(data: List<GalleryInfo>) {}
protected open fun onRemoveData(data: List<E>) {} protected open fun onRemoveData(data: List<GalleryInfo>) {}
protected open fun onClearData() {} protected open fun onClearData() {}
fun onGetPageData( fun onGetPageData(
taskId: Int, taskId: Int,
@@ -328,7 +329,7 @@ class ContentLayout(context: Context, attrs: AttributeSet? = null) : FrameLayout
nextPage: Int, nextPage: Int,
prev: String?, prev: String?,
next: String?, next: String?,
data: List<E>, data: List<GalleryInfo>,
) { ) {
if (mCurrentTaskId == taskId) { if (mCurrentTaskId == taskId) {
val dataSize: Int val dataSize: Int
@@ -734,7 +735,11 @@ class ContentLayout(context: Context, attrs: AttributeSet? = null) : FrameLayout
val shownView = mViewTransition.shownViewIndex val shownView = mViewTransition.shownViewIndex
bundle.putInt(KEY_SHOWN_VIEW, shownView) bundle.putInt(KEY_SHOWN_VIEW, shownView)
bundle.putString(KEY_TIP, binding.tip.text.toString()) bundle.putString(KEY_TIP, binding.tip.text.toString())
bundle.putParcelableArrayList(KEY_DATA, mData) synchronized(contentMap) {
val size = contentMap.keys.takeIf { it.isNotEmpty() }?.max()?.inc() ?: 0
contentMap[size] = mData
bundle.putInt(KEY_DATA, size)
}
bundle.putInt(KEY_NEXT_ID, mIdGenerator.nextId()) bundle.putInt(KEY_NEXT_ID, mIdGenerator.nextId())
bundle.putParcelable(KEY_PAGE_DIVIDER, mPageDivider) bundle.putParcelable(KEY_PAGE_DIVIDER, mPageDivider)
bundle.putInt(KEY_START_PAGE, mStartPage) bundle.putInt(KEY_START_PAGE, mStartPage)
@@ -749,11 +754,12 @@ class ContentLayout(context: Context, attrs: AttributeSet? = null) : FrameLayout
return if (state is Bundle) { return if (state is Bundle) {
mViewTransition.showView(state.getInt(KEY_SHOWN_VIEW), false) mViewTransition.showView(state.getInt(KEY_SHOWN_VIEW), false)
binding.tip.text = state.getString(KEY_TIP) binding.tip.text = state.getString(KEY_TIP)
@Suppress("DEPRECATION") val data = state.getInt(KEY_DATA)
val newData = state.getParcelableArrayList<E>(KEY_DATA) synchronized(contentMap) {
newData?.let { mData = it } mData = contentMap.remove(data) ?: ArrayList()
}
mIdGenerator.setNextId(state.getInt(KEY_NEXT_ID)) mIdGenerator.setNextId(state.getInt(KEY_NEXT_ID))
mPageDivider = state.getParcelableCompat(KEY_PAGE_DIVIDER)!! mPageDivider = state.getParcelableCompat(KEY_PAGE_DIVIDER) ?: IntList()
mStartPage = state.getInt(KEY_START_PAGE) mStartPage = state.getInt(KEY_START_PAGE)
mEndPage = state.getInt(KEY_END_PAGE) mEndPage = state.getInt(KEY_END_PAGE)
pages = state.getInt(KEY_PAGES) pages = state.getInt(KEY_PAGES)
@@ -761,7 +767,7 @@ class ContentLayout(context: Context, attrs: AttributeSet? = null) : FrameLayout
mNext = state.getString(KEY_NEXT) mNext = state.getString(KEY_NEXT)
notifyDataSetChanged() notifyDataSetChanged()
notifyDataSetChanged {} notifyDataSetChanged {}
if (newData == null) { if (mData.size == 0) {
mPageDivider.clear() mPageDivider.clear()
mStartPage = 0 mStartPage = 0
mEndPage = 0 mEndPage = 0
@@ -803,3 +809,8 @@ class ContentLayout(context: Context, attrs: AttributeSet? = null) : FrameLayout
} }
} }
} }
// data "ArrayList<GalleryInfo>" is too large, we cannot put it as Parcelable otherwise TransactionTooLargeException would occur
// TODO: convert to paging3 source then keep it in a proper window
// @see PagingConfig.maxSize
private val contentMap = hashMapOf<Int, ArrayList<GalleryInfo>>()
@@ -28,7 +28,7 @@ import java.time.ZoneOffset
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.util.Locale import java.util.Locale
abstract class GalleryInfoContentHelper : ContentHelper<GalleryInfo>() { abstract class GalleryInfoContentHelper : ContentHelper() {
@JvmField @JvmField
var jumpTo: String? = null var jumpTo: String? = null
private val listener: FavouriteStatusRouter.Listener private val listener: FavouriteStatusRouter.Listener
@@ -54,7 +54,7 @@ class ImageSearchLayout @JvmOverloads constructor(
Mdc3Theme { Mdc3Theme {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
fun selectImage() = coroutineScope.launch { fun selectImage() = coroutineScope.launch {
pickVisualMedia(ImageOnly)?.let { context.pickVisualMedia(ImageOnly)?.let {
path = it.toString() path = it.toString()
} }
} }
@@ -19,7 +19,6 @@ import android.graphics.drawable.AnimatedImageDrawable
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import coil.imageLoader import coil.imageLoader
import coil.request.ImageRequest import coil.request.ImageRequest
import coil.size.Size
import com.hippo.ehviewer.coil.imageRequest import com.hippo.ehviewer.coil.imageRequest
class UnikeryDrawable(private val mTextView: ObservedTextView, url: String) : class UnikeryDrawable(private val mTextView: ObservedTextView, url: String) :
@@ -31,7 +30,6 @@ class UnikeryDrawable(private val mTextView: ObservedTextView, url: String) :
mTextView.setOnWindowAttachListener(this) mTextView.setOnWindowAttachListener(this)
mImageRequest = mTextView.context.imageRequest { mImageRequest = mTextView.context.imageRequest {
target(onSuccess = { onGetValue(it) }) target(onSuccess = { onGetValue(it) })
size(Size.ORIGINAL)
data(url) data(url)
} }
load() load()
@@ -59,6 +59,7 @@ import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhUtils import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.coil.imageRequest import com.hippo.ehviewer.coil.imageRequest
import com.hippo.ehviewer.coil.read
import com.hippo.ehviewer.dao.DownloadInfo import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.databinding.DrawerListRvBinding import com.hippo.ehviewer.databinding.DrawerListRvBinding
import com.hippo.ehviewer.databinding.ItemDownloadBinding import com.hippo.ehviewer.databinding.ItemDownloadBinding
@@ -912,8 +913,8 @@ class DownloadsScene :
if (!thumbLocation.exists()) { if (!thumbLocation.exists()) {
thumbLocation.ensureFile() thumbLocation.ensureFile()
val key = info.thumbKey!! val key = info.thumbKey!!
imageCache[key]?.use { imageCache.read(key) {
UniFile.fromFile(it.data.toFile())!!.openFileDescriptor("r").use { src -> UniFile.fromFile(data.toFile())!!.openFileDescriptor("r").use { src ->
thumbLocation.openFileDescriptor("w").use { dst -> thumbLocation.openFileDescriptor("w").use { dst ->
src sendTo dst src sendTo dst
} }
@@ -116,6 +116,7 @@ import coil.imageLoader
import com.google.android.material.progressindicator.CircularProgressIndicator import com.google.android.material.progressindicator.CircularProgressIndicator
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.hippo.ehviewer.EhApplication.Companion.galleryDetailCache import com.hippo.ehviewer.EhApplication.Companion.galleryDetailCache
import com.hippo.ehviewer.EhApplication.Companion.imageCache
import com.hippo.ehviewer.EhDB import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.R import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings import com.hippo.ehviewer.Settings
@@ -139,6 +140,7 @@ import com.hippo.ehviewer.client.parser.ParserUtils
import com.hippo.ehviewer.client.parser.TorrentParser import com.hippo.ehviewer.client.parser.TorrentParser
import com.hippo.ehviewer.coil.imageRequest import com.hippo.ehviewer.coil.imageRequest
import com.hippo.ehviewer.coil.justDownload import com.hippo.ehviewer.coil.justDownload
import com.hippo.ehviewer.coil.read
import com.hippo.ehviewer.dao.DownloadInfo import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.dao.Filter import com.hippo.ehviewer.dao.Filter
import com.hippo.ehviewer.download.DownloadManager.DownloadInfoListener import com.hippo.ehviewer.download.DownloadManager.DownloadInfoListener
@@ -1217,7 +1219,7 @@ class GalleryDetailScene : BaseScene(), DownloadInfoListener {
} }
try { try {
val key = composeBindingGI!!.thumbKey!! val key = composeBindingGI!!.thumbKey!!
val path = imageLoader(context).diskCache!![key]!!.use { it.data } val path = imageCache.read(key) { data }
val lub = ListUrlBuilder() val lub = ListUrlBuilder()
lub.mode = ListUrlBuilder.MODE_IMAGE_SEARCH lub.mode = ListUrlBuilder.MODE_IMAGE_SEARCH
lub.imagePath = path.toString() lub.imagePath = path.toString()
@@ -57,6 +57,7 @@ import com.hippo.ehviewer.coil.imageRequest
import com.hippo.ehviewer.coil.justDownload import com.hippo.ehviewer.coil.justDownload
import com.hippo.ehviewer.ui.compose.data.EhPreviewItem import com.hippo.ehviewer.ui.compose.data.EhPreviewItem
import com.hippo.ehviewer.ui.compose.rememberDialogState import com.hippo.ehviewer.ui.compose.rememberDialogState
import com.hippo.ehviewer.ui.compose.rememberMemorized
import com.hippo.ehviewer.ui.compose.setMD3Content import com.hippo.ehviewer.ui.compose.setMD3Content
import com.hippo.ehviewer.ui.legacy.recyclerview.calculateSuitableSpanCount import com.hippo.ehviewer.ui.legacy.recyclerview.calculateSuitableSpanCount
import com.hippo.ehviewer.ui.navToReader import com.hippo.ehviewer.ui.navToReader
@@ -72,7 +73,7 @@ class GalleryPreviewScreen : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return ComposeView(inflater.context).apply { return ComposeView(inflater.context).apply {
setMD3Content { setMD3Content {
val galleryDetail = rememberSaveable { requireArguments().getParcelableCompat<GalleryDetail>(KEY_GALLERY_DETAIL)!! } val galleryDetail = remember { requireArguments().getParcelableCompat<GalleryDetail>(KEY_GALLERY_DETAIL)!! }
val context = LocalContext.current val context = LocalContext.current
fun onPreviewCLick(index: Int) = context.navToReader(galleryDetail, index) fun onPreviewCLick(index: Int) = context.navToReader(galleryDetail, index)
val toNextPage = rememberSaveable { requireArguments().getBoolean(KEY_NEXT_PAGE) } val toNextPage = rememberSaveable { requireArguments().getBoolean(KEY_NEXT_PAGE) }
@@ -118,14 +119,14 @@ class GalleryPreviewScreen : Fragment() {
initialKey = goto initialKey = goto
} }
val previewPagesMap = rememberSaveable { mutableMapOf<Int, PreviewPage>().apply { put(1, galleryDetail.previewList) } } val previewPagesMap = rememberMemorized { mutableMapOf<Int, PreviewPage>().apply { put(1, galleryDetail.previewList) } }
val data = remember(initialKey) { val data = remember(initialKey) {
Pager(PagingConfig(1, prefetchDistance = 4, enablePlaceholders = false, initialLoadSize = 1), initialKey) { Pager(PagingConfig(1, prefetchDistance = 4, enablePlaceholders = false, initialLoadSize = 1), initialKey) {
object : PagingSource<Int, PreviewPage>() { object : PagingSource<Int, PreviewPage>() {
override fun getRefreshKey(state: PagingState<Int, PreviewPage>) = null override fun getRefreshKey(state: PagingState<Int, PreviewPage>) = null
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, PreviewPage> { override suspend fun load(params: LoadParams<Int>): LoadResult<Int, PreviewPage> {
val up = params.key ?: 1 val up = params.key ?: 1
val end = up + params.loadSize - 1 val end = (up + params.loadSize - 1).coerceAtMost(galleryDetail.previewPages)
runSuspendCatching { runSuspendCatching {
(up..end).mapNotNull { it.takeUnless { previewPagesMap.contains(it) } } (up..end).mapNotNull { it.takeUnless { previewPagesMap.contains(it) } }
.parMap(Dispatchers.IO) { getPreviewListByPage(it - 1).apply { previewPagesMap[it] = this } } .parMap(Dispatchers.IO) { getPreviewListByPage(it - 1).apply { previewPagesMap[it] = this } }
@@ -1,41 +1,69 @@
package com.hippo.ehviewer.util package com.hippo.ehviewer.util
import android.content.Context import android.content.Context
import android.content.ContextWrapper
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContract
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import arrow.atomic.Atomic import androidx.lifecycle.Lifecycle
import arrow.atomic.update import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
import arrow.atomic.AtomicInt
import eu.kanade.tachiyomi.util.lang.withUIContext import eu.kanade.tachiyomi.util.lang.withUIContext
import kotlinx.coroutines.suspendCancellableCoroutine import rikka.core.util.ContextUtils.requireActivity
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
// Fuck off the silly Android launcher and callback :) // Fuck off the silly Android launcher and callback :)
private typealias LauncherAndCallback<K, V> = Atomic<Pair<ActivityResultLauncher<K>, (V) -> Unit>> private val atomicInteger = AtomicInt()
private fun <K, V> LauncherAndCallback<K, V>.cleanup() = update { it.copy {} }
private lateinit var requestPermission: LauncherAndCallback<String, Boolean>
private lateinit var pickVisualMedia: LauncherAndCallback<PickVisualMediaRequest, Uri?>
private suspend fun <K, V> LauncherAndCallback<K, V>.await(key: K) = withUIContext {
suspendCancellableCoroutine { cont ->
updateAndGet { prev -> prev.copy { cont.resume(it) } }.first.launch(key)
cont.invokeOnCancellation { cleanup() } // Drop continuation when cancelled
}.apply { cleanup() } // Drop continuation when completed
}
fun ComponentActivity.initLauncher() { private val Context.lifecycle: Lifecycle
requestPermission = Atomic(registerForActivityResult(ActivityResultContracts.RequestPermission()) { requestPermission.get().second(it) } to {}) get() {
pickVisualMedia = Atomic(registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { pickVisualMedia.get().second(it) } to {}) var context: Context? = this
while (true) {
when (context) {
is LifecycleOwner -> return context.lifecycle
!is ContextWrapper -> error("This should never happen!")
else -> context = context.baseContext
}
}
}
private suspend fun <I, O> Context.awaitActivityResult(contract: ActivityResultContract<I, O>, input: I): O {
val key = "activity_rq#${atomicInteger.getAndIncrement()}"
var launcher: ActivityResultLauncher<I>? = null
var observer: LifecycleEventObserver? = null
observer = LifecycleEventObserver { _, event ->
if (Lifecycle.Event.ON_DESTROY == event) {
launcher?.unregister()
if (observer != null) {
lifecycle.removeObserver(observer!!)
}
}
}
lifecycle.addObserver(observer)
return withUIContext {
suspendCoroutine { cont -> // No cancellation support here since we cannot cancel a launched Intent
val activity = requireActivity<ComponentActivity>(this@awaitActivityResult)
launcher = activity.activityResultRegistry.register(key, contract) {
launcher?.unregister()
lifecycle.removeObserver(observer)
cont.resume(it)
}.apply { launch(input) }
}
}
} }
suspend fun Context.requestPermission(key: String): Boolean { suspend fun Context.requestPermission(key: String): Boolean {
if (ContextCompat.checkSelfPermission(this, key) == PackageManager.PERMISSION_GRANTED) return true if (ContextCompat.checkSelfPermission(this, key) == PackageManager.PERMISSION_GRANTED) return true
return requestPermission.await(key) return awaitActivityResult(ActivityResultContracts.RequestPermission(), key)
} }
suspend fun pickVisualMedia(type: VisualMediaType): Uri? = pickVisualMedia.await(PickVisualMediaRequest.Builder().setMediaType(type).build()) suspend fun Context.pickVisualMedia(type: VisualMediaType): Uri? = awaitActivityResult(ActivityResultContracts.PickVisualMedia(), PickVisualMediaRequest.Builder().setMediaType(type).build())
+1 -1
View File
@@ -15,6 +15,6 @@ tasks.register("Delete", Delete::class) {
buildscript { buildscript {
dependencies { dependencies {
classpath("com.android.tools:r8:8.2.6-dev") classpath("com.android.tools:r8:8.2.7-dev")
} }
} }