Shows Image
This commit is contained in:
106
app/src/main/java/xyz/quaver/pupil/util/ImageCache.kt
Normal file
106
app/src/main/java/xyz/quaver/pupil/util/ImageCache.kt
Normal file
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2021 tom5079
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.util
|
||||
|
||||
import android.content.Context
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.util.*
|
||||
import io.ktor.utils.io.*
|
||||
import io.ktor.utils.io.core.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import org.kodein.di.DIAware
|
||||
import org.kodein.di.android.closestDI
|
||||
import org.kodein.di.instance
|
||||
import xyz.quaver.hitomi.sha256
|
||||
import java.io.File
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.text.toByteArray
|
||||
|
||||
class NetworkCache(context: Context) : DIAware {
|
||||
override val di by closestDI(context)
|
||||
|
||||
private val client: HttpClient by instance()
|
||||
|
||||
private val cacheDir = context.cacheDir
|
||||
|
||||
private val _channels = ConcurrentHashMap<String, Channel<Float>>()
|
||||
val channels = _channels as Map<String, Channel<Float>>
|
||||
|
||||
private val requests = mutableMapOf<String, Job>()
|
||||
|
||||
private val networkScope = CoroutineScope(Executors.newFixedThreadPool(4).asCoroutineDispatcher())
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
suspend fun load(requestBuilder: HttpRequestBuilder.() -> Unit): File = coroutineScope {
|
||||
val request = HttpRequestBuilder().apply(requestBuilder)
|
||||
|
||||
val url = request.url.buildString()
|
||||
val hash = sha256(url.toByteArray()).joinToString("") { "%02x".format(it) }
|
||||
|
||||
val file = File(cacheDir, "$hash.${url.takeLastWhile { it != '.' }}")
|
||||
|
||||
val progressChannel = if (_channels[url]?.isClosedForSend == false)
|
||||
_channels[url]!!
|
||||
else
|
||||
Channel<Float>(1, BufferOverflow.DROP_OLDEST).also { _channels[url] = it }
|
||||
|
||||
if (file.exists())
|
||||
progressChannel.close()
|
||||
else
|
||||
requests[url] = networkScope.launch {
|
||||
kotlin.runCatching {
|
||||
file.createNewFile()
|
||||
|
||||
client.request<HttpStatement>(request).execute { httpResponse ->
|
||||
val responseChannel: ByteReadChannel = httpResponse.receive()
|
||||
val contentLength = httpResponse.contentLength() ?: -1
|
||||
var readBytes = 0f
|
||||
|
||||
file.outputStream().use { outputStream ->
|
||||
while (!responseChannel.isClosedForRead) {
|
||||
val packet = responseChannel.readRemaining(DEFAULT_BUFFER_SIZE.toLong())
|
||||
while (!packet.isEmpty) {
|
||||
val bytes = packet.readBytes()
|
||||
outputStream.write(bytes)
|
||||
|
||||
readBytes += bytes.size
|
||||
progressChannel.trySend(readBytes / contentLength)
|
||||
}
|
||||
}
|
||||
}
|
||||
progressChannel.close()
|
||||
}
|
||||
}.onFailure {
|
||||
file.delete()
|
||||
FirebaseCrashlytics.getInstance().recordException(it)
|
||||
progressChannel.close(it)
|
||||
}
|
||||
}
|
||||
|
||||
return@coroutineScope file
|
||||
}
|
||||
}
|
||||
@@ -19,17 +19,33 @@
|
||||
package xyz.quaver.pupil.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.BitmapRegionDecoder
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.toAndroidRect
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import kotlinx.serialization.json.*
|
||||
import org.kodein.di.DIAware
|
||||
import org.kodein.di.DirectDIAware
|
||||
import org.kodein.di.direct
|
||||
import org.kodein.di.instance
|
||||
import xyz.quaver.graphics.subsampledimage.ImageSource
|
||||
import xyz.quaver.graphics.subsampledimage.newBitmapRegionDecoder
|
||||
import xyz.quaver.io.FileX
|
||||
import xyz.quaver.io.util.inputStream
|
||||
import xyz.quaver.pupil.db.AppDatabase
|
||||
import xyz.quaver.pupil.sources.ItemInfo
|
||||
import xyz.quaver.pupil.sources.SourceEntries
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.util.*
|
||||
@@ -136,4 +152,20 @@ fun View.hide() {
|
||||
|
||||
fun View.show() {
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
class FileXImageSource(file: FileX): ImageSource {
|
||||
private val decoder = newBitmapRegionDecoder(file.inputStream()!!)
|
||||
|
||||
override val imageSize by lazy { Size(decoder.width.toFloat(), decoder.height.toFloat()) }
|
||||
|
||||
override fun decodeRegion(region: Rect, sampleSize: Int): ImageBitmap =
|
||||
decoder.decodeRegion(region.toAndroidRect(), BitmapFactory.Options().apply {
|
||||
inSampleSize = sampleSize
|
||||
}).asImageBitmap()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberFileXImageSource(file: FileX) = remember {
|
||||
FileXImageSource(file)
|
||||
}
|
||||
Reference in New Issue
Block a user