Shows Image
This commit is contained in:
@@ -132,7 +132,7 @@ dependencies {
|
||||
implementation("xyz.quaver:libpupil:2.1.11")
|
||||
implementation("xyz.quaver:documentfilex:0.7.1")
|
||||
implementation("xyz.quaver:floatingsearchview:1.1.7")
|
||||
implementation("xyz.quaver:subsampledimage:0.0.1-alpha01-SNAPSHOT")
|
||||
implementation("xyz.quaver:subsampledimage:0.0.1-alpha09-SNAPSHOT")
|
||||
|
||||
implementation("org.kodein.log:kodein-log:0.11.1")
|
||||
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.7")
|
||||
|
||||
@@ -58,6 +58,7 @@ class Pupil : Application(), DIAware {
|
||||
import(sourceModule)
|
||||
|
||||
bind { singleton { DownloadManager(applicationContext) } }
|
||||
bind { singleton { NetworkCache(applicationContext) } }
|
||||
|
||||
bind { singleton {
|
||||
HttpClient(OkHttp) {
|
||||
|
||||
@@ -21,6 +21,7 @@ package xyz.quaver.pupil.sources
|
||||
import android.app.Application
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.runtime.Composable
|
||||
import io.ktor.http.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.kodein.di.*
|
||||
@@ -61,7 +62,7 @@ abstract class Source {
|
||||
@Composable
|
||||
open fun SearchResult(itemInfo: ItemInfo, onEvent: ((SearchResultEvent) -> Unit)? = null) { }
|
||||
|
||||
open fun getHeadersForImage(itemID: String, url: String): Map<String, String> = emptyMap()
|
||||
open fun getHeadersBuilderForImage(itemID: String, url: String): HeadersBuilder.() -> Unit = { }
|
||||
|
||||
open fun onSuggestionBind(binding: SearchSuggestionItemBinding, item: SearchSuggestion) {
|
||||
binding.leftIcon.setImageResource(R.drawable.tag)
|
||||
|
||||
@@ -190,7 +190,7 @@ class Hitomi(app: Application) : Source(), DIAware {
|
||||
val reader = getGalleryInfo(galleryID)
|
||||
|
||||
return reader.files.map {
|
||||
imageUrlFromImage(galleryID, it, true)
|
||||
imageUrlFromImage(galleryID, it, false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,9 +225,9 @@ class Hitomi(app: Application) : Source(), DIAware {
|
||||
FullSearchResult(itemInfo = itemInfo)
|
||||
}
|
||||
|
||||
override fun getHeadersForImage(itemID: String, url: String) = mapOf(
|
||||
"Referer" to getReferer(itemID.toInt())
|
||||
)
|
||||
override fun getHeadersBuilderForImage(itemID: String, url: String): HeadersBuilder.() -> Unit = {
|
||||
append("Referer", getReferer(itemID.toInt()))
|
||||
}
|
||||
|
||||
override fun onSuggestionBind(binding: SearchSuggestionItemBinding, item: SearchSuggestion) {
|
||||
item as TagSuggestion
|
||||
|
||||
@@ -24,40 +24,44 @@ import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.BrokenImage
|
||||
import androidx.compose.material.icons.filled.Fullscreen
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import coil.annotation.ExperimentalCoilApi
|
||||
import coil.compose.rememberImagePainter
|
||||
import coil.request.ImageRequest
|
||||
import coil.transform.BlurTransformation
|
||||
import com.google.accompanist.appcompattheme.AppCompatTheme
|
||||
import io.ktor.http.*
|
||||
import okhttp3.Headers
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.kodein.di.DIAware
|
||||
import org.kodein.di.android.closestDI
|
||||
import org.kodein.log.LoggerFactory
|
||||
import org.kodein.log.newLogger
|
||||
import xyz.quaver.graphics.subsampledimage.SubSampledImage
|
||||
import xyz.quaver.graphics.subsampledimage.*
|
||||
import xyz.quaver.io.FileX
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.ui.composable.FloatingActionButtonState
|
||||
import xyz.quaver.pupil.ui.composable.MultipleFloatingActionButton
|
||||
import xyz.quaver.pupil.ui.composable.SubFabItem
|
||||
import xyz.quaver.pupil.ui.viewmodel.ReaderViewModel
|
||||
import xyz.quaver.pupil.util.FileXImageSource
|
||||
import kotlin.math.abs
|
||||
|
||||
class ReaderActivity : ComponentActivity(), DIAware {
|
||||
override val di by closestDI()
|
||||
@@ -78,10 +82,31 @@ class ReaderActivity : ComponentActivity(), DIAware {
|
||||
val isFullscreen by model.isFullscreen.observeAsState(false)
|
||||
val title by model.title.observeAsState(stringResource(R.string.reader_loading))
|
||||
val sourceIcon by model.sourceIcon.observeAsState()
|
||||
val images by model.images.observeAsState(emptyList())
|
||||
val source by model.sourceInstance.observeAsState()
|
||||
val imageSources = remember { mutableStateListOf<ImageSource?>() }
|
||||
val imageHeights = remember { mutableStateListOf<Float?>() }
|
||||
val states = remember { mutableStateListOf<SubSampledImageState>() }
|
||||
|
||||
logger.debug { "target: ${R.drawable.hitomi} value: $sourceIcon" }
|
||||
LaunchedEffect(model.totalProgress) {
|
||||
if (imageSources.isEmpty() && model.imageList.isNotEmpty())
|
||||
imageSources.addAll(List(model.imageList.size) { null })
|
||||
|
||||
if (states.isEmpty() && model.imageList.isNotEmpty())
|
||||
states.addAll(List(model.imageList.size) { SubSampledImageState(ScaleTypes.FIT_WIDTH, Bounds.FORCE_OVERLAP_OR_CENTER) })
|
||||
|
||||
if (imageHeights.isEmpty() && model.imageList.isNotEmpty())
|
||||
imageHeights.addAll(List(model.imageList.size) { null })
|
||||
|
||||
model.imageList.forEachIndexed { i, image ->
|
||||
if (imageSources[i] == null && image != null)
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
imageSources[i] = kotlin.runCatching {
|
||||
FileXImageSource(FileX(this@ReaderActivity, image))
|
||||
}.onFailure {
|
||||
model.error(i)
|
||||
}.getOrNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WindowInsetsControllerCompat(window, window.decorView).run {
|
||||
if (isFullscreen) {
|
||||
@@ -121,7 +146,7 @@ class ReaderActivity : ComponentActivity(), DIAware {
|
||||
icon = Icons.Default.Fullscreen,
|
||||
label = stringResource(id = R.string.reader_fab_fullscreen)
|
||||
) {
|
||||
model.isFullscreen.postValue(!isFullscreen)
|
||||
model.isFullscreen.postValue(true)
|
||||
}
|
||||
),
|
||||
targetState = isFABExpanded,
|
||||
@@ -132,14 +157,57 @@ class ReaderActivity : ComponentActivity(), DIAware {
|
||||
}
|
||||
) {
|
||||
LazyColumn(
|
||||
verticalArrangement = Arrangement.spacedBy(32.dp)
|
||||
Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
items(images) { image ->
|
||||
SubSampledImage(
|
||||
modifier = Modifier.fillMaxWidth().heightIn(128.dp, 1000.dp)
|
||||
)
|
||||
itemsIndexed(imageSources) { i, imageSource ->
|
||||
LaunchedEffect(states[i].canvasSize, states[i].imageSize) {
|
||||
if (imageHeights.isNotEmpty() && imageHeights[i] == null)
|
||||
states[i].canvasSize?.let { canvasSize ->
|
||||
states[i].imageSize?.let { imageSize ->
|
||||
imageHeights[i] = imageSize.height * canvasSize.width / imageSize.width
|
||||
} }
|
||||
}
|
||||
|
||||
Box(
|
||||
Modifier
|
||||
.height(
|
||||
imageHeights
|
||||
.getOrNull(i)
|
||||
?.let { with(LocalDensity.current) { it.toDp() } }
|
||||
?: 500.dp)
|
||||
.fillMaxWidth()
|
||||
.border(1.dp, Color.Gray),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (imageSource == null)
|
||||
model.progressList.getOrNull(i)?.let { progress ->
|
||||
if (progress < 0f)
|
||||
Icon(Icons.Filled.BrokenImage, null)
|
||||
else
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
LinearProgressIndicator(progress)
|
||||
Text((i + 1).toString())
|
||||
}
|
||||
}
|
||||
else
|
||||
SubSampledImage(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
imageSource = imageSource,
|
||||
state = states[i]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (model.totalProgress != model.imageCount)
|
||||
LinearProgressIndicator(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
progress = model.progressList.map { abs(it) }.sum() / model.progressList.size,
|
||||
color = colorResource(id = R.color.colorAccent)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package xyz.quaver.pupil.ui.composable
|
||||
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
@@ -17,6 +19,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
@@ -72,16 +75,17 @@ fun MiniFloatingActionButton(
|
||||
}
|
||||
}
|
||||
|
||||
FloatingActionButton(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.scale(buttonScale),
|
||||
onClick = { onClick?.invoke(item) },
|
||||
elevation = elevation,
|
||||
interactionSource = interactionSource
|
||||
) {
|
||||
Icon(item.icon, contentDescription = null)
|
||||
}
|
||||
if (buttonScale > 0f)
|
||||
FloatingActionButton(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.scale(buttonScale),
|
||||
onClick = { onClick?.invoke(item) },
|
||||
elevation = elevation,
|
||||
interactionSource = interactionSource
|
||||
) {
|
||||
Icon(item.icon, contentDescription = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,8 +21,17 @@ package xyz.quaver.pupil.ui.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.http.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import org.kodein.di.DIAware
|
||||
import org.kodein.di.android.x.closestDI
|
||||
import org.kodein.di.direct
|
||||
@@ -33,6 +42,7 @@ import xyz.quaver.pupil.db.AppDatabase
|
||||
import xyz.quaver.pupil.db.Bookmark
|
||||
import xyz.quaver.pupil.db.History
|
||||
import xyz.quaver.pupil.sources.Source
|
||||
import xyz.quaver.pupil.util.NetworkCache
|
||||
import xyz.quaver.pupil.util.source
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@@ -40,6 +50,8 @@ class ReaderViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
||||
|
||||
override val di by closestDI()
|
||||
|
||||
private val cache: NetworkCache by instance()
|
||||
|
||||
private val logger = newLogger(LoggerFactory.default)
|
||||
|
||||
val isFullscreen = MutableLiveData(false)
|
||||
@@ -58,8 +70,14 @@ class ReaderViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
||||
private val _title = MutableLiveData<String>()
|
||||
val title = _title as LiveData<String>
|
||||
|
||||
private val _images = MutableLiveData<List<String>>()
|
||||
val images: LiveData<List<String>> = _images
|
||||
private val totalProgressMutex = Mutex()
|
||||
var totalProgress by mutableStateOf(0)
|
||||
private set
|
||||
var imageCount by mutableStateOf(0)
|
||||
private set
|
||||
|
||||
val imageList = mutableStateListOf<Uri?>()
|
||||
val progressList = mutableStateListOf<Float>()
|
||||
|
||||
val isBookmarked = Transformations.switchMap(MediatorLiveData<Pair<Source, String>>().apply {
|
||||
addSource(source) { source -> itemID.value?.let { itemID -> source to itemID } }
|
||||
@@ -119,12 +137,66 @@ class ReaderViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
_images.postValue(withContext(Dispatchers.IO) {
|
||||
withContext(Dispatchers.IO) {
|
||||
source.images(itemID)
|
||||
})
|
||||
}.let { images ->
|
||||
imageCount = images.size
|
||||
|
||||
progressList.addAll(List(imageCount) { 0f })
|
||||
imageList.addAll(List(imageCount) { null })
|
||||
|
||||
images.forEachIndexed { index, image ->
|
||||
when (val scheme = image.takeWhile { it != ':' }) {
|
||||
"http", "https" -> {
|
||||
val file = cache.load {
|
||||
url(image)
|
||||
headers(source.getHeadersBuilderForImage(itemID, image))
|
||||
}
|
||||
|
||||
val channel = cache.channels[image] ?: error("Channel is null")
|
||||
|
||||
if (channel.isClosedForReceive) {
|
||||
imageList[index] = Uri.fromFile(file)
|
||||
totalProgressMutex.withLock {
|
||||
totalProgress++
|
||||
}
|
||||
} else {
|
||||
channel.invokeOnClose { e ->
|
||||
viewModelScope.launch {
|
||||
if (e == null) {
|
||||
imageList[index] = Uri.fromFile(file)
|
||||
totalProgressMutex.withLock {
|
||||
totalProgress++
|
||||
}
|
||||
} else {
|
||||
TODO("Handle error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
kotlin.runCatching {
|
||||
for (progress in channel) {
|
||||
progressList[index] = progress
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"content" -> {
|
||||
progressList[index] = 1f
|
||||
}
|
||||
else -> throw IllegalArgumentException("Expected URL scheme 'http(s)' or 'content' but was '$scheme'")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun error(index: Int) {
|
||||
progressList[index] = -1f
|
||||
}
|
||||
|
||||
fun toggleBookmark() {
|
||||
val bookmark = source.value?.let { source -> itemID.value?.let { itemID -> Bookmark(source, itemID) } } ?: return
|
||||
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -13,7 +13,7 @@ buildscript {
|
||||
classpath("com.google.gms:google-services:4.3.10")
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
classpath("com.google.firebase:firebase-crashlytics-gradle:2.7.1")
|
||||
classpath("com.google.firebase:firebase-crashlytics-gradle:2.8.0")
|
||||
classpath("com.google.firebase:perf-plugin:1.4.0")
|
||||
classpath("com.google.android.gms:oss-licenses-plugin:0.10.4")
|
||||
}
|
||||
@@ -23,6 +23,7 @@ allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
mavenLocal()
|
||||
maven { url = uri("https://oss.sonatype.org/content/repositories/snapshots") }
|
||||
maven { url = uri("https://jitpack.io") }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user