Compare commits
37
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afbe0a4de5 | ||
|
|
0a5cdf7c80 | ||
|
|
9d689923ac | ||
|
|
98c5ae7490 | ||
|
|
868ac22c69 | ||
|
|
636ac64692 | ||
|
|
a9eec6a783 | ||
|
|
00867c7dc1 | ||
|
|
cf79cdc7c1 | ||
|
|
25d4d5e393 | ||
|
|
1743caa869 | ||
|
|
958fdb19b4 | ||
|
|
4b63648a79 | ||
|
|
498b84df37 | ||
|
|
7a4e65259d | ||
|
|
8cb8dcbe0f | ||
|
|
da37b3cdff | ||
|
|
0a1768e481 | ||
|
|
818d16a32d | ||
|
|
e9a5d1cc7c | ||
|
|
dac56f3e32 | ||
|
|
d6cbcdc6cd | ||
|
|
c5c1170383 | ||
|
|
6ed3fb29d9 | ||
|
|
74c6bfa479 | ||
|
|
d44981a70c | ||
|
|
06e411d332 | ||
|
|
f419189749 | ||
|
|
aa1e2102fc | ||
|
|
306ce9ed21 | ||
|
|
544fcb2e88 | ||
|
|
cb01df4071 | ||
|
|
38310b7c5a | ||
|
|
fcd56ff4e9 | ||
|
|
41300a9fd4 | ||
|
|
0bd5f9dc31 | ||
|
|
004ecd0dd3 |
No files matched your search
+15
-13
@@ -57,8 +57,8 @@ android {
|
||||
applicationId = "moe.tarsin.ehviewer"
|
||||
minSdk = 28
|
||||
targetSdk = 33
|
||||
versionCode = 180038
|
||||
versionName = "1.8.8.0-beta"
|
||||
versionCode = 180042
|
||||
versionName = "1.8.8.3"
|
||||
resourceConfigurations.addAll(
|
||||
listOf(
|
||||
"zh",
|
||||
@@ -158,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
|
||||
@@ -186,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")
|
||||
|
||||
@@ -207,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")
|
||||
@@ -219,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)
|
||||
@@ -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@saveToCache))
|
||||
}
|
||||
}.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 {
|
||||
|
||||
@@ -39,7 +39,7 @@ fun EhAsyncCropThumb(
|
||||
key: GalleryInfo,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var contentScale by remember { mutableStateOf(ContentScale.Fit) }
|
||||
var contentScale by remember(key.gid) { mutableStateOf(ContentScale.Fit) }
|
||||
AsyncImage(
|
||||
model = requestOf(key),
|
||||
contentDescription = null,
|
||||
|
||||
@@ -129,7 +129,7 @@ fun GalleryInfoGridItem(
|
||||
info: GalleryInfo,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val aspect = (info.thumbWidth.toFloat() / info.thumbHeight).coerceIn(0.33F, 1.5F)
|
||||
val aspect = (info.thumbWidth.toFloat() / info.thumbHeight).coerceIn(0.33F, 1.5F).takeUnless { it.isNaN() } ?: 1F
|
||||
val color = EhUtils.getCategoryColor(info.category)
|
||||
val simpleLang = info.simpleLanguage
|
||||
ElevatedCard(
|
||||
|
||||
@@ -44,7 +44,7 @@ fun EhAsyncPreview(
|
||||
model: GalleryPreview,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var contentScale by remember { mutableStateOf(ContentScale.Fit) }
|
||||
var contentScale by remember(model.imageKey) { mutableStateOf(ContentScale.Fit) }
|
||||
AsyncImage(
|
||||
model = requestOf(model),
|
||||
contentDescription = null,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -878,63 +883,18 @@ class DownloadsScene :
|
||||
fun bind(info: DownloadInfo) {
|
||||
lifecycleScope.launchIO {
|
||||
val downloadDir = SpiderDen.getGalleryDownloadDir(info.gid) ?: run {
|
||||
val title = EhUtils.getSuitableTitle(info)
|
||||
val dirname = FileUtils.sanitizeFilename("${info.gid}-$title")
|
||||
EhDB.putDownloadDirname(info.gid, dirname)
|
||||
info.putToDownloadDir()
|
||||
SpiderDen.getGalleryDownloadDir(info.gid)!!
|
||||
}
|
||||
check(downloadDir.ensureDir())
|
||||
downloadDir.ensureDir()
|
||||
val thumbLocation = downloadDir.subFile(".thumb")!!
|
||||
val localReq = thumbLocation.takeIf { it.exists() }?.uri?.let {
|
||||
context?.imageRequest {
|
||||
data(it.toString())
|
||||
memoryCacheKey(info.thumbKey)
|
||||
}
|
||||
}
|
||||
withUIContext {
|
||||
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,
|
||||
CompanionAsyncThumb(
|
||||
info = info,
|
||||
path = thumbLocation,
|
||||
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 {
|
||||
runCatching {
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1286,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(info.gid) { 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()
|
||||
|
||||
@@ -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())
|
||||
Reference in new issue
Block a user