Compare commits

...
51 Commits
Author SHA1 Message Date
tarsin 636ac64692 Release 1.8.8.2
Releases / Build (push) Has been cancelled
2023-05-26 14:35:53 +08:00
tarsin a9eec6a783 Fixup! Resolve #1113 2023-05-26 14:35:30 +08:00
tarsin 00867c7dc1 Release 1.8.8.1 bugfix version
Releases / Build (push) Has been cancelled
2023-05-26 13:46:38 +08:00
tarsin cf79cdc7c1 Resolve #1113 2023-05-26 13:45:49 +08:00
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
tarsin 746a045328 Bump to 1.8.8.0-alpha
Releases / Build (push) Has been cancelled
2023-05-17 22:49:34 +08:00
tarsin 2a4c7823c2 Bind downloadHolder with coroutine 2023-05-17 22:42:55 +08:00
tarsin b995510df1 Resolve #1082, #1081 again 2023-05-17 22:21:17 +08:00
tarsin a2852d6df9 Resolve #1082, #1081 again 2023-05-17 19:08:07 +08:00
tarsin 47863580a0 Resolve #1082, #1081 again 2023-05-17 19:01:39 +08:00
tarsin 9af6532331 Resolve #1082, #1081 2023-05-17 16:28:50 +08:00
21 changed files with 325 additions and 188 deletions

No files matched your search

+17 -13
View File
@@ -57,8 +57,8 @@ android {
applicationId = "moe.tarsin.ehviewer"
minSdk = 28
targetSdk = 33
versionCode = 180038
versionName = "1.8.8.0-preview"
versionCode = 180041
versionName = "1.8.8.2"
resourceConfigurations.addAll(
listOf(
"zh",
@@ -95,6 +95,8 @@ android {
freeCompilerArgs = listOf(
// https://kotlinlang.org/docs/compiler-reference.html#progressive
"-progressive",
"-Xjvm-default=all",
"-Xlambdas=indy",
"-opt-in=coil.annotation.ExperimentalCoilApi",
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
@@ -156,22 +158,22 @@ android {
dependencies {
// 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.biometric:biometric-ktx:1.2.0-alpha05")
implementation("androidx.browser:browser:1.5.0")
implementation("androidx.collection:collection-ktx:1.3.0-alpha04")
// https://developer.android.com/jetpack/androidx/releases/compose-material3
// api(platform("androidx.compose:compose-bom:2023.03.00"))
api(platform("dev.chrisbanes.compose:compose-bom:2023.04.00-SNAPSHOT"))
// api(platform("androidx.compose:compose-bom:2023.05.00"))
api(platform("dev.chrisbanes.compose:compose-bom:2023.04.00-beta01"))
implementation("androidx.compose.material:material-icons-extended")
implementation("androidx.compose.material3:material3")
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.drawerlayout:drawerlayout:1.2.0")
// https://developer.android.com/jetpack/androidx/releases/lifecycle
@@ -184,8 +186,8 @@ dependencies {
implementation("androidx.navigation:navigation-compose:$nav_version")
// https://developer.android.com/jetpack/androidx/releases/paging
implementation("androidx.paging:paging-common-ktx:3.2.0-alpha05")
implementation("androidx.paging:paging-compose:1.0.0-alpha19")
implementation("androidx.paging:paging-common-ktx:3.2.0-alpha06")
implementation("androidx.paging:paging-compose:1.0.0-alpha20")
implementation("androidx.preference:preference-ktx:1.2.0")
implementation("androidx.recyclerview:recyclerview:1.3.0")
@@ -205,7 +207,7 @@ dependencies {
implementation("com.google.android.material:material:1.9.0")
// 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.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.material:material-preference:2.0.0")
implementation(platform("io.arrow-kt:arrow-stack:2.0.0-SNAPSHOT"))
implementation("io.arrow-kt:arrow-functions")
implementation(platform("io.arrow-kt:arrow-stack:1.2.0-RC"))
implementation("io.arrow-kt:arrow-fx-coroutines")
// 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-serialization-cbor:1.5.1")
implementation("org.jsoup:jsoup:1.16.1")
@@ -25,6 +25,7 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import coil.ImageLoader
import coil.ImageLoaderFactory
import coil.decode.ImageDecoderDecoder
import coil.util.DebugLogger
import com.hippo.ehviewer.client.EhCookieStore
import com.hippo.ehviewer.client.EhDns
@@ -137,7 +138,10 @@ class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactor
override fun newImageLoader(): ImageLoader {
return ImageLoader.Builder(this).apply {
okHttpClient(nonCacheOkHttpClient)
components { add(MergeInterceptor) }
components {
add { result, options, _ -> ImageDecoderDecoder(result.source, options, false) }
add(MergeInterceptor)
}
diskCache(imageCache)
crossfade(300)
error(R.drawable.image_failed)
@@ -23,6 +23,8 @@ import java.net.Socket
import javax.net.ssl.SSLSocket
import javax.net.ssl.SSLSocketFactory
private const val EXCEPTIONAL_DOMAIN = "hath.network"
object EhSSLSocketFactory : SSLSocketFactory() {
private val sslSocketFactory = getDefault() as SSLSocketFactory
override fun getDefaultCipherSuites(): Array<String> {
@@ -34,7 +36,7 @@ object EhSSLSocketFactory : SSLSocketFactory() {
}
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 sslSession = socket.session
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 com.hippo.ehviewer.client.data.GalleryInfo
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.url
@@ -19,14 +20,13 @@ fun ImageRequest.Builder.ehUrl(info: GalleryInfo) = apply {
data(info.thumbUrl)
memoryCacheKey(key)
diskCacheKey(key)
size(Size.ORIGINAL)
}
fun ImageRequest.Builder.ehPreview(preview: GalleryPreview) = apply {
data(preview.url)
memoryCacheKey(preview.imageKey)
diskCacheKey(preview.imageKey)
size(Size.ORIGINAL)
if (preview is NormalGalleryPreview) size(Size.ORIGINAL)
}
private val stubResult = DecodeResult(ColorDrawable(Color.BLACK), false)
@@ -55,16 +55,14 @@ class SpiderDen(private val mGalleryInfo: GalleryInfo) {
set(value) {
field = value
if (field == SpiderQueen.MODE_DOWNLOAD && downloadDir == null) {
val title = getSuitableTitle(mGalleryInfo)
val dirname = FileUtils.sanitizeFilename("$mGid-$title")
EhDB.putDownloadDirname(mGid, dirname)
mGalleryInfo.putToDownloadDir()
downloadDir = getGalleryDownloadDir(mGid)!!.apply { check(ensureDir()) { "Download directory $uri is not valid directory!" } }
}
}
private fun containInCache(index: Int): Boolean {
val key = getImageKey(mGid, index)
return sCache[key]?.use { true } ?: false
return sCache.read(key) { true } ?: false
}
private fun containInDownloadDir(index: Int): Boolean {
@@ -84,11 +82,11 @@ class SpiderDen(private val mGalleryInfo: GalleryInfo) {
it sendTo outFd
}
}
}
}.getOrElse {
true
} ?: false
}.onFailure {
it.printStackTrace()
false
}
}.getOrDefault(false)
}
operator fun contains(index: Int): Boolean {
@@ -199,12 +197,11 @@ class SpiderDen(private val mGalleryInfo: GalleryInfo) {
val key = getImageKey(mGid, index)
// Read from diskCache first
sCache[key]?.use { snapshot ->
sCache.read(key) {
runCatching {
UniFile.fromFile(snapshot.data.toFile())!!.openFileDescriptor("r").use {
UniFile.fromFile(data.toFile())!!.openFileDescriptor("r").use {
it sendTo toFd
}
}.onSuccess {
return true
}.onFailure {
it.printStackTrace()
@@ -235,15 +232,14 @@ class SpiderDen(private val mGalleryInfo: GalleryInfo) {
fun getExtension(index: Int): String? {
val key = getImageKey(mGid, index)
return sCache[key]?.use { it.metadata.toNioPath().readText() }
?: downloadDir?.let { findImageFile(it, index) }
?.name.let { FileUtils.getExtensionFromFilename(it) }
return sCache.read(key) { metadata.toNioPath().readText() }
?: downloadDir?.let { findImageFile(it, index) }?.name.let { FileUtils.getExtensionFromFilename(it) }
}
fun getImageSource(index: Int): CloseableSource? {
if (mode == SpiderQueen.MODE_READ) {
val key = getImageKey(mGid, index)
val snapshot = sCache[key]
val snapshot = sCache.openSnapshot(key)
if (snapshot != null) {
val source = ImageDecoder.createSource(snapshot.data.toFile())
return object : CloseableSource, AutoCloseable by snapshot {
@@ -288,3 +284,9 @@ private fun fixExtension(extension: String): String {
private fun findImageFile(dir: UniFile, index: Int): UniFile? {
return COMPAT_IMAGE_EXTENSIONS.firstNotNullOfOrNull { dir.findFile(perFilename(index, it)) }
}
fun GalleryInfo.putToDownloadDir() {
val title = getSuitableTitle(this)
val dirname = FileUtils.sanitizeFilename("$gid-$title")
EhDB.putDownloadDirname(gid, dirname)
}
@@ -2,6 +2,8 @@ package com.hippo.ehviewer.spider
import coil.disk.DiskCache
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.unifile.UniFile
import com.hippo.unifile.openInputStream
@@ -10,6 +12,7 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.cbor.Cbor
import kotlinx.serialization.decodeFromByteArray
import kotlinx.serialization.encodeToByteArray
import moe.tarsin.coroutines.runSuspendCatching
import java.io.File
@Serializable
@@ -35,15 +38,13 @@ fun SpiderInfo.write(file: UniFile) {
}
}
fun SpiderInfo.saveToCache() {
val entry = spiderInfoCache.edit(gid.toString()) ?: return
runCatching {
entry.data.toFile().writeBytes(Cbor.encodeToByteArray(this))
suspend fun SpiderInfo.saveToCache() {
runSuspendCatching {
spiderInfoCache.edit(gid.toString()) {
data.toFile().writeBytes(Cbor.encodeToByteArray(this))
}
}.onFailure {
it.printStackTrace()
entry.abort()
}.onSuccess {
entry.commit()
}
}
@@ -54,14 +55,13 @@ private val spiderInfoCache by lazy {
}
fun readFromCache(gid: Long): SpiderInfo? {
val snapshot = spiderInfoCache[gid.toString()] ?: return null
return runCatching {
snapshot.use {
return Cbor.decodeFromByteArray(it.data.toFile().readBytes())
}
}.onFailure {
it.printStackTrace()
}.getOrNull()
return spiderInfoCache.read(gid.toString()) {
runCatching {
Cbor.decodeFromByteArray<SpiderInfo>(data.toFile().readBytes())
}.onFailure {
it.printStackTrace()
}.getOrNull()
}
}
fun readCompatFromUniFile(file: UniFile): SpiderInfo? {
@@ -49,6 +49,7 @@ import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.Semaphore
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
}
}
}
?: 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)
@@ -427,7 +427,7 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS
private fun writeSpiderInfoToLocal() {
if (!isReady) return
mSpiderDen.downloadDir?.run { createFile(SPIDER_INFO_FILENAME).also { mSpiderInfo.write(it) } }
mSpiderInfo.saveToCache()
runBlocking { mSpiderInfo.saveToCache() }
}
private fun isStateDone(state: Int): Boolean {
@@ -18,9 +18,12 @@ package com.hippo.ehviewer.ui
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
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.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
@@ -103,7 +106,10 @@ fun GalleryInfoBottomSheet(
}
}
ModalBottomSheet(onDismissRequest) {
ModalBottomSheet(
onDismissRequest = onDismissRequest,
windowInsets = WindowInsets(0),
) {
check(data.size == keys.size)
val context = LocalContext.current
val navController = LocalNavController.current
@@ -114,23 +120,25 @@ fun GalleryInfoBottomSheet(
Text(text = stringResource(id = R.string.gallery_info), style = MaterialTheme.typography.titleLarge)
}
CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.labelLarge) {
keys.forEachIndexed { index, i ->
val key = stringResource(i)
Row(
modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.keyline_margin)).clickable {
if (index == INDEX_PARENT) {
data[index]?.let { navController.navWithUrl(it) }
} else {
context tellClipboardWithToast data[index]
if (index == INDEX_URL) {
// Save it to avoid detect the gallery
Settings.putClipboardTextHashCode(data[index].hashCode())
LazyColumn {
itemsIndexed(keys) { index, i ->
val key = stringResource(i)
Row(
modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.keyline_margin)).clickable {
if (index == INDEX_PARENT) {
data[index]?.let { navController.navWithUrl(it) }
} else {
context tellClipboardWithToast data[index]
if (index == INDEX_URL) {
// Save it to avoid detect the gallery
Settings.putClipboardTextHashCode(data[index].hashCode())
}
}
}
}.fillMaxWidth(),
) {
Text(key, modifier = Modifier.width(90.dp).padding(8.dp))
Text(data[index].orEmpty(), modifier = Modifier.padding(8.dp))
}.fillMaxWidth(),
) {
Text(key, modifier = Modifier.width(90.dp).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.getParcelableExtraCompat
import com.hippo.ehviewer.util.getUrlFromClipboard
import com.hippo.ehviewer.util.initLauncher
import com.hippo.ehviewer.yorozuya.IOUtils
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.util.lang.launchUI
@@ -91,10 +90,6 @@ class MainActivity : EhActivity() {
private val connectivityManager by lazy { getSystemService<ConnectivityManager>()!! }
private val availableNetworks = mutableListOf<Network>()
init {
initLauncher()
}
private fun saveImageToTempFile(uri: Uri): File? {
val src = ImageDecoder.createSource(contentResolver, uri)
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.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
import com.hippo.ehviewer.R
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.databinding.WidgetContentLayoutBinding
import com.hippo.ehviewer.ui.legacy.ViewTransition.OnShowViewListener
import com.hippo.ehviewer.ui.legacy.easyrecyclerview.HandlerDrawable
@@ -41,7 +42,7 @@ import com.hippo.ehviewer.yorozuya.collect.IntList
import rikka.core.res.resolveColor
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 mFastScrollerOriginBottom: Int
private val binding: WidgetContentLayoutBinding
@@ -64,7 +65,7 @@ class ContentLayout(context: Context, attrs: AttributeSet? = null) : FrameLayout
val fastScroller
get() = binding.fastScroller
fun setHelper(helper: ContentHelper<*>) {
fun setHelper(helper: ContentHelper) {
mContentHelper = helper
helper.init(this)
}
@@ -128,7 +129,7 @@ class ContentLayout(context: Context, attrs: AttributeSet? = null) : FrameLayout
super.onRestoreInstanceState(mContentHelper.restoreInstanceState(state))
}
abstract class ContentHelper<E : Parcelable> : OnShowViewListener {
abstract class ContentHelper : OnShowViewListener {
/**
* Generate task id
*/
@@ -143,7 +144,7 @@ class ContentLayout(context: Context, attrs: AttributeSet? = null) : FrameLayout
/**
* Store data
*/
private var mData = ArrayList<E>()
private var mData = ArrayList<GalleryInfo>()
/**
* Store the page divider index
@@ -287,10 +288,10 @@ class ContentLayout(context: Context, attrs: AttributeSet? = null) : FrameLayout
mEmptyString = str
}
val data: List<E>
val data: List<GalleryInfo>
get() = mData
fun getDataAtEx(location: Int): E? {
fun getDataAtEx(location: Int): GalleryInfo? {
return if (location >= 0 && location < mData.size) {
mData[location]
} 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))
fun size(): Int {
@@ -309,8 +310,8 @@ class ContentLayout(context: Context, attrs: AttributeSet? = null) : FrameLayout
return mCurrentTaskId == taskId
}
protected abstract fun isDuplicate(d1: E, d2: E): Boolean
private fun removeDuplicateData(data: List<E>, start: Int, end: Int) {
protected abstract fun isDuplicate(d1: GalleryInfo, d2: GalleryInfo): Boolean
private fun removeDuplicateData(data: List<GalleryInfo>, start: Int, end: Int) {
val slicedData = mData.slice(start.coerceAtLeast(0) until end.coerceAtMost(mData.size))
data.dropWhile { d1 ->
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 onRemoveData(data: List<E>) {}
protected open fun onAddData(data: List<GalleryInfo>) {}
protected open fun onRemoveData(data: List<GalleryInfo>) {}
protected open fun onClearData() {}
fun onGetPageData(
taskId: Int,
@@ -328,7 +329,7 @@ class ContentLayout(context: Context, attrs: AttributeSet? = null) : FrameLayout
nextPage: Int,
prev: String?,
next: String?,
data: List<E>,
data: List<GalleryInfo>,
) {
if (mCurrentTaskId == taskId) {
val dataSize: Int
@@ -734,7 +735,11 @@ class ContentLayout(context: Context, attrs: AttributeSet? = null) : FrameLayout
val shownView = mViewTransition.shownViewIndex
bundle.putInt(KEY_SHOWN_VIEW, shownView)
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.putParcelable(KEY_PAGE_DIVIDER, mPageDivider)
bundle.putInt(KEY_START_PAGE, mStartPage)
@@ -749,11 +754,12 @@ class ContentLayout(context: Context, attrs: AttributeSet? = null) : FrameLayout
return if (state is Bundle) {
mViewTransition.showView(state.getInt(KEY_SHOWN_VIEW), false)
binding.tip.text = state.getString(KEY_TIP)
@Suppress("DEPRECATION")
val newData = state.getParcelableArrayList<E>(KEY_DATA)
newData?.let { mData = it }
val data = state.getInt(KEY_DATA)
synchronized(contentMap) {
mData = contentMap.remove(data) ?: ArrayList()
}
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)
mEndPage = state.getInt(KEY_END_PAGE)
pages = state.getInt(KEY_PAGES)
@@ -761,7 +767,7 @@ class ContentLayout(context: Context, attrs: AttributeSet? = null) : FrameLayout
mNext = state.getString(KEY_NEXT)
notifyDataSetChanged()
notifyDataSetChanged {}
if (newData == null) {
if (mData.size == 0) {
mPageDivider.clear()
mStartPage = 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.util.Locale
abstract class GalleryInfoContentHelper : ContentHelper<GalleryInfo>() {
abstract class GalleryInfoContentHelper : ContentHelper() {
@JvmField
var jumpTo: String? = null
private val listener: FavouriteStatusRouter.Listener
@@ -54,7 +54,7 @@ class ImageSearchLayout @JvmOverloads constructor(
Mdc3Theme {
val coroutineScope = rememberCoroutineScope()
fun selectImage() = coroutineScope.launch {
pickVisualMedia(ImageOnly)?.let {
context.pickVisualMedia(ImageOnly)?.let {
path = it.toString()
}
}
@@ -19,7 +19,6 @@ import android.graphics.drawable.AnimatedImageDrawable
import android.graphics.drawable.Drawable
import coil.imageLoader
import coil.request.ImageRequest
import coil.size.Size
import com.hippo.ehviewer.coil.imageRequest
class UnikeryDrawable(private val mTextView: ObservedTextView, url: String) :
@@ -31,7 +30,6 @@ class UnikeryDrawable(private val mTextView: ObservedTextView, url: String) :
mTextView.setOnWindowAttachListener(this)
mImageRequest = mTextView.context.imageRequest {
target(onSuccess = { onGetValue(it) })
size(Size.ORIGINAL)
data(url)
}
load()
@@ -32,6 +32,7 @@ import androidx.appcompat.app.AlertDialog
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.height
import androidx.compose.material3.Card
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -39,6 +40,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.core.view.GravityCompat
@@ -58,7 +60,9 @@ import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.coil.imageRequest
import com.hippo.ehviewer.coil.read
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.databinding.DrawerListRvBinding
import com.hippo.ehviewer.databinding.ItemDownloadBinding
@@ -69,6 +73,7 @@ import com.hippo.ehviewer.download.DownloadManager.DownloadInfoListener
import com.hippo.ehviewer.download.DownloadService
import com.hippo.ehviewer.download.DownloadService.Companion.clear
import com.hippo.ehviewer.spider.SpiderDen
import com.hippo.ehviewer.spider.putToDownloadDir
import com.hippo.ehviewer.ui.compose.CropDefaults
import com.hippo.ehviewer.ui.compose.data.requestOf
import com.hippo.ehviewer.ui.compose.setMD3Content
@@ -876,57 +881,23 @@ class DownloadsScene :
private val height = (Settings.listThumbSize * 3).pxToDp.dp
fun bind(info: DownloadInfo) {
val downloadDir = SpiderDen.getGalleryDownloadDir(info.gid)?.takeIf { it.isDirectory }
check(downloadDir != null)
val thumbLocation = downloadDir.subFile(".thumb")!!
val localReq = thumbLocation.takeIf { it.exists() }?.uri?.let {
context?.imageRequest {
data(it.toString())
memoryCacheKey(info.thumbKey)
lifecycleScope.launchIO {
val downloadDir = SpiderDen.getGalleryDownloadDir(info.gid) ?: run {
info.putToDownloadDir()
SpiderDen.getGalleryDownloadDir(info.gid)!!
}
}
binding.thumb.setMD3Content {
Card(onClick = ::onClick.partially1(binding.thumb)) {
var contentScale by remember { mutableStateOf(ContentScale.Fit) }
val coroutineScope = rememberCoroutineScope { Dispatchers.IO }
AsyncImage(
model = localReq ?: requestOf(info),
contentDescription = null,
modifier = Modifier.height(height).aspectRatio(0.6666667F),
onState = { state ->
if (state is AsyncImagePainter.State.Success) {
state.result.drawable.run {
if (CropDefaults.shouldCrop(intrinsicWidth, intrinsicHeight)) {
contentScale = ContentScale.Crop
}
}
coroutineScope.launch {
if (!thumbLocation.exists()) {
thumbLocation.ensureFile()
val key = info.thumbKey!!
imageCache[key]?.use {
UniFile.fromFile(it.data.toFile())!!.openFileDescriptor("r").use { src ->
thumbLocation.openFileDescriptor("w").use { dst ->
src sendTo dst
}
}
}
}
}
}
if (state is AsyncImagePainter.State.Error) {
if (thumbLocation.exists()) {
coroutineScope.launch {
thumbLocation.delete()
withUIContext {
bind(info)
}
}
}
}
},
contentScale = contentScale,
)
downloadDir.ensureDir()
val thumbLocation = downloadDir.subFile(".thumb")!!
withUIContext {
binding.thumb.setMD3Content {
Card(onClick = ::onClick.partially1(binding.thumb)) {
CompanionAsyncThumb(
info = info,
path = thumbLocation,
modifier = Modifier.height(height).aspectRatio(0.6666667F),
)
}
}
}
}
binding.title.text = EhUtils.getSuitableTitle(info)
@@ -1275,3 +1246,61 @@ class DownloadsScene :
private const val PAYLOAD_STATE = 0
}
}
@Composable
private fun CompanionAsyncThumb(
info: GalleryInfo,
path: UniFile,
modifier: Modifier = Modifier,
) {
var contentScale by remember { mutableStateOf(ContentScale.Fit) }
val coroutineScope = rememberCoroutineScope { Dispatchers.IO }
val context = LocalContext.current
var localReq by remember(info.gid) {
path.takeIf { it.isFile }?.uri?.let {
context.imageRequest {
data(it.toString())
memoryCacheKey(info.thumbKey)
}
}.let { mutableStateOf(it) }
}
AsyncImage(
model = localReq ?: requestOf(info),
contentDescription = null,
modifier = modifier,
onState = { state ->
if (state is AsyncImagePainter.State.Success) {
state.result.drawable.run {
if (CropDefaults.shouldCrop(intrinsicWidth, intrinsicHeight)) {
contentScale = ContentScale.Crop
}
}
coroutineScope.launch {
runCatching {
if (!path.exists() && path.ensureFile()) {
val key = info.thumbKey!!
imageCache.read(key) {
UniFile.fromFile(data.toFile())!!.openFileDescriptor("r").use { src ->
path.openFileDescriptor("w").use { dst ->
src sendTo dst
}
}
}
}
}.onFailure {
it.printStackTrace()
}
}
}
if (state is AsyncImagePainter.State.Error) {
coroutineScope.launch {
if (path.exists()) {
path.delete()
localReq = null
}
}
}
},
contentScale = contentScale,
)
}
@@ -116,6 +116,7 @@ import coil.imageLoader
import com.google.android.material.progressindicator.CircularProgressIndicator
import com.google.android.material.snackbar.Snackbar
import com.hippo.ehviewer.EhApplication.Companion.galleryDetailCache
import com.hippo.ehviewer.EhApplication.Companion.imageCache
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.R
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.coil.imageRequest
import com.hippo.ehviewer.coil.justDownload
import com.hippo.ehviewer.coil.read
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.dao.Filter
import com.hippo.ehviewer.download.DownloadManager.DownloadInfoListener
@@ -1217,7 +1219,7 @@ class GalleryDetailScene : BaseScene(), DownloadInfoListener {
}
try {
val key = composeBindingGI!!.thumbKey!!
val path = imageLoader(context).diskCache!![key]!!.use { it.data }
val path = imageCache.read(key) { data }
val lub = ListUrlBuilder()
lub.mode = ListUrlBuilder.MODE_IMAGE_SEARCH
lub.imagePath = path.toString()
@@ -57,6 +57,7 @@ import com.hippo.ehviewer.coil.imageRequest
import com.hippo.ehviewer.coil.justDownload
import com.hippo.ehviewer.ui.compose.data.EhPreviewItem
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.legacy.recyclerview.calculateSuitableSpanCount
import com.hippo.ehviewer.ui.navToReader
@@ -72,7 +73,7 @@ class GalleryPreviewScreen : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return ComposeView(inflater.context).apply {
setMD3Content {
val galleryDetail = rememberSaveable { requireArguments().getParcelableCompat<GalleryDetail>(KEY_GALLERY_DETAIL)!! }
val galleryDetail = remember { requireArguments().getParcelableCompat<GalleryDetail>(KEY_GALLERY_DETAIL)!! }
val context = LocalContext.current
fun onPreviewCLick(index: Int) = context.navToReader(galleryDetail, index)
val toNextPage = rememberSaveable { requireArguments().getBoolean(KEY_NEXT_PAGE) }
@@ -118,14 +119,14 @@ class GalleryPreviewScreen : Fragment() {
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) {
Pager(PagingConfig(1, prefetchDistance = 4, enablePlaceholders = false, initialLoadSize = 1), initialKey) {
object : PagingSource<Int, PreviewPage>() {
override fun getRefreshKey(state: PagingState<Int, PreviewPage>) = null
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, PreviewPage> {
val up = params.key ?: 1
val end = up + params.loadSize - 1
val end = (up + params.loadSize - 1).coerceAtMost(galleryDetail.previewPages)
runSuspendCatching {
(up..end).mapNotNull { it.takeUnless { previewPagesMap.contains(it) } }
.parMap(Dispatchers.IO) { getPreviewListByPage(it - 1).apply { previewPagesMap[it] = this } }
@@ -1,41 +1,69 @@
package com.hippo.ehviewer.util
import android.content.Context
import android.content.ContextWrapper
import android.content.pm.PackageManager
import android.net.Uri
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContract
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType
import androidx.core.content.ContextCompat
import arrow.atomic.Atomic
import arrow.atomic.update
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
import arrow.atomic.AtomicInt
import eu.kanade.tachiyomi.util.lang.withUIContext
import kotlinx.coroutines.suspendCancellableCoroutine
import rikka.core.util.ContextUtils.requireActivity
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
// Fuck off the silly Android launcher and callback :)
private typealias LauncherAndCallback<K, V> = Atomic<Pair<ActivityResultLauncher<K>, (V) -> Unit>>
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
}
private val atomicInteger = AtomicInt()
fun ComponentActivity.initLauncher() {
requestPermission = Atomic(registerForActivityResult(ActivityResultContracts.RequestPermission()) { requestPermission.get().second(it) } to {})
pickVisualMedia = Atomic(registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { pickVisualMedia.get().second(it) } to {})
private val Context.lifecycle: Lifecycle
get() {
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 {
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 {
dependencies {
classpath("com.android.tools:r8:8.2.6-dev")
classpath("com.android.tools:r8:8.2.7-dev")
}
}