Image caching

This commit is contained in:
shabinder 2021-02-06 20:16:26 +05:30
parent 3ae3b404b1
commit e63a5d97fd
13 changed files with 211 additions and 36 deletions

View File

@ -9,12 +9,30 @@ import androidx.compose.runtime.MutableState
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.Dp
import androidx.core.net.toUri
import com.shabinder.common.Picture
import com.shabinder.common.database.appContext
import dev.chrisbanes.accompanist.coil.CoilImage
@Composable
actual fun ImageLoad(
pic: Picture?,
modifier: Modifier
){
Image(pic?.image?.asImageBitmap(), vectorResource(R.drawable.music) ,"Image",modifier)
}
@Composable
fun Image(pic: ImageBitmap?, placeholder:ImageVector, desc: String,modifier:Modifier = Modifier) {
if(pic == null) Image(placeholder,desc,modifier) else Image(pic,desc,modifier)
}
/*
@Composable
actual fun ImageLoad(
url:String,
@ -31,6 +49,7 @@ actual fun ImageLoad(
modifier = modifier
)
}
*/
@Composable
actual fun Toast(

View File

@ -1,10 +1,9 @@
package com.shabinder.common.list
import androidx.compose.runtime.Composable
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.mvikotlin.core.store.StoreFactory
import com.shabinder.common.FetchPlatformQueryResult
import com.shabinder.common.PlatformQueryResult
import com.shabinder.common.TrackDetails
import com.shabinder.common.*
import com.shabinder.common.list.integration.SpotiFlyerListImpl
import com.shabinder.common.utils.Consumer
import com.shabinder.database.Database
@ -28,9 +27,15 @@ interface SpotiFlyerList {
* */
fun onBackPressed()
/*
* Load Image from cache/Internet and cache it
* */
fun loadImage(url:String):Picture?
interface Dependencies {
val storeFactory: StoreFactory
val fetchQuery: FetchPlatformQueryResult
val dir: Dir
val link: String
fun listOutput(finished: Output.Finished): Consumer<Output>
}

View File

@ -1,5 +1,6 @@
package com.shabinder.common.list
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.CircularProgressIndicator
@ -18,6 +19,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.shabinder.common.DownloadStatus
import com.shabinder.common.Picture
import com.shabinder.common.TrackDetails
import com.shabinder.common.ui.ImageLoad
import com.shabinder.spotiflyer.ui.SpotiFlyerTypography
@ -40,12 +42,13 @@ fun SpotiFlyerListContent(
verticalArrangement = Arrangement.spacedBy(8.dp),
content = {
item {
CoverImage(result.title, result.coverUrl, coroutineScope)
CoverImage(result.title, result.coverUrl, coroutineScope,component::loadImage)
}
itemsIndexed(result.trackList) { index, item ->
TrackCard(
track = item,
downloadTrack = { component.onDownloadClicked(result.trackList,index) },
loadImage = component::loadImage
)
}
},
@ -62,10 +65,12 @@ fun SpotiFlyerListContent(
fun TrackCard(
track: TrackDetails,
downloadTrack:()->Unit,
loadImage:(String)->Picture?
) {
Row(verticalAlignment = Alignment.CenterVertically,modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp)) {
val pic:Picture? = loadImage(track.albumArtURL)
ImageLoad(
url = track.albumArtURL,
pic = pic,
modifier = Modifier
.preferredWidth(75.dp)
.preferredHeight(90.dp)
@ -112,14 +117,16 @@ fun CoverImage(
title: String,
coverURL: String,
scope: CoroutineScope,
loadImage: (String) -> Picture?,
modifier: Modifier = Modifier,
) {
Column(
modifier.padding(vertical = 8.dp).fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
val pic = loadImage(coverURL)
ImageLoad(
url = coverURL,
pic,
modifier = Modifier
.preferredWidth(210.dp)
.preferredHeight(230.dp)

View File

@ -2,6 +2,7 @@ package com.shabinder.common.list.integration
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.mvikotlin.extensions.coroutines.states
import com.shabinder.common.Picture
import com.shabinder.common.TrackDetails
import com.shabinder.common.list.SpotiFlyerList
import com.shabinder.common.list.SpotiFlyerList.Dependencies
@ -38,4 +39,6 @@ internal class SpotiFlyerListImpl(
override fun onBackPressed(){
listOutput(SpotiFlyerList.Output.Finished)
}
override fun loadImage(url: String): Picture? = dir.loadImage(url)
}

View File

@ -4,6 +4,7 @@ import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.RouterState
import com.arkivanov.decompose.value.Value
import com.arkivanov.mvikotlin.core.store.StoreFactory
import com.shabinder.common.Dir
import com.shabinder.common.FetchPlatformQueryResult
import com.shabinder.common.list.SpotiFlyerList
import com.shabinder.common.main.SpotiFlyerMain
@ -24,6 +25,7 @@ interface SpotiFlyerRoot {
val storeFactory: StoreFactory
val database: Database
val fetchPlatformQueryResult: FetchPlatformQueryResult
val directories: Dir
}
}

View File

@ -8,6 +8,7 @@ import com.arkivanov.decompose.router
import com.arkivanov.decompose.statekeeper.Parcelable
import com.arkivanov.decompose.statekeeper.Parcelize
import com.arkivanov.decompose.value.Value
import com.shabinder.common.Dir
import com.shabinder.common.list.SpotiFlyerList
import com.shabinder.common.main.SpotiFlyerMain
import com.shabinder.common.root.SpotiFlyerRoot
@ -48,6 +49,7 @@ internal class SpotiFlyerRootImpl(
componentContext = componentContext,
dependencies = object : SpotiFlyerList.Dependencies, Dependencies by this {
override val fetchQuery = fetchPlatformQueryResult
override val dir: Dir = directories
override val link: String = link
override fun listOutput(finished: SpotiFlyerList.Output.Finished): Consumer<SpotiFlyerList.Output> =

View File

@ -3,13 +3,13 @@ package com.shabinder.common.ui
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.Dp
import com.shabinder.common.Picture
@Composable
expect fun ImageLoad(
url:String,
loadingResource: ImageBitmap? = null,
errorResource: ImageBitmap? = null,
pic: Picture?,
modifier: Modifier = Modifier
)

View File

@ -2,14 +2,11 @@ package com.shabinder.common.ui
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.unit.Dp
import com.shabinder.common.Picture
@Composable
actual fun ImageLoad(
url:String,
loadingResource: ImageBitmap?,
errorResource: ImageBitmap?,
pic: Picture?,
modifier: Modifier
){

View File

@ -0,0 +1,21 @@
<!--
~ Copyright (c) 2021 Shabinder Singh
~ 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/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="42dp"
android:height="42dp" android:viewportWidth="512" android:viewportHeight="512">
<path android:fillColor="#A3787878" android:pathData="m511.739,103.734 l-257,50.947v233.725c-10.733,-7.199 -23.633,-11.406 -37.5,-11.406 -37.22,0 -67.5,30.28 -67.5,67.5s30.28,67.5 67.5,67.5c34.684,0 63.329,-26.299 67.073,-60h0.427v-182.682l197,-39.053v98.141c-10.733,-7.199 -23.633,-11.406 -37.5,-11.406 -37.22,0 -67.5,30.28 -67.5,67.5s30.28,67.5 67.5,67.5c39.927,0 71.547,-34.762 67.073,-75h0.427zM217.239,482c-20.678,0 -37.5,-16.822 -37.5,-37.5s16.822,-37.5 37.5,-37.5 37.5,16.822 37.5,37.5 -16.822,37.5 -37.5,37.5zM444.239,422c-20.678,0 -37.5,-16.822 -37.5,-37.5s16.822,-37.5 37.5,-37.5 37.5,16.822 37.5,37.5 -16.822,37.5 -37.5,37.5zM481.739,199.682 L284.739,238.735v-59.416l197,-39.053z"/>
<path android:fillColor="#A3787878" android:pathData="m182.179,159.75h30c0,-31.002 4.415,-66.799 -24.144,-95.356 -8.968,-8.968 -17.455,-16.07 -24.942,-22.336 -19.798,-16.57 -27.832,-24.012 -27.832,-42.058h-30v221.406c-10.734,-7.199 -23.634,-11.406 -37.5,-11.406 -37.22,0 -67.5,30.28 -67.5,67.5s30.28,67.5 67.5,67.5c34.684,0 63.329,-26.299 67.073,-60h0.427v-227.219c9.458,8.262 20.077,16.341 31.562,27.825 19.029,19.031 15.356,44.009 15.356,74.144zM67.761,315c-20.678,0 -37.5,-16.822 -37.5,-37.5s16.822,-37.5 37.5,-37.5 37.5,16.822 37.5,37.5 -16.823,37.5 -37.5,37.5z"/>
</vector>

View File

@ -2,11 +2,15 @@ package com.shabinder.common
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Environment
import co.touchlab.kermit.Kermit
import com.mpatric.mp3agic.Mp3File
import com.shabinder.common.database.appContext
import java.io.*
import java.lang.Exception
import java.net.HttpURLConnection
import java.net.URL
import java.nio.charset.StandardCharsets
actual open class Dir actual constructor(
@ -87,4 +91,69 @@ actual open class Dir actual constructor(
.setId3v1Tags(trackDetails)
.setId3v2TagsAndSaveFile(trackDetails,path)
}
actual fun loadImage(url: String, cachePath: String):Picture? {
var picture: Picture? = loadCachedImage(cachePath)
if (picture == null) picture = freshImage(url,cachePath)
return picture
}
private fun loadCachedImage(cachePath: String): Picture? {
return try {
val read = BufferedReader(
InputStreamReader(
FileInputStream(cachePath + cacheImagePostfix()),
StandardCharsets.UTF_8
)
)
val source = read.readLine()
val width = read.readLine().toInt()
val height = read.readLine().toInt()
read.close()
val result: Bitmap? = BitmapFactory.decodeFile(cachePath)
if (result != null) {
Picture(
source,
getNameURL(source),
result,
width,
height
)
}else null
} catch (e: Exception) {
e.printStackTrace()
null
}
}
private fun freshImage(url:String,cachePath: String):Picture?{
return try {
val source = URL(url)
val connection: HttpURLConnection = source.openConnection() as HttpURLConnection
connection.connectTimeout = 5000
connection.connect()
val input: InputStream = connection.inputStream
val result: Bitmap? = BitmapFactory.decodeStream(input)
if (result != null) {
val picture = Picture(
url,
getNameURL(url),
result,
result.width,
result.height
)
cacheImage(picture)
picture
} else null
} catch (e: Exception) {
e.printStackTrace()
null
}
}
}

View File

@ -18,31 +18,11 @@ expect open class Dir(
fun imageCacheDir(): String
fun createDirectory(dirPath:String)
fun cacheImage(picture: Picture)
fun loadImage(url:String, cachePath:String = imageCacheDir() + getNameURL(url)):Picture?
suspend fun clearCache()
suspend fun saveFileWithMetadata(mp3ByteArray: ByteArray, path: String, trackDetails: TrackDetails)
}
suspend fun Dir.downloadFile(url: String): Flow<DownloadResult> {
return flow {
val client = createHttpClient()
val response = client.get<HttpStatement>(url).execute()
val data = ByteArray(response.contentLength()!!.toInt())
var offset = 0
do {
val currentRead = response.content.readAvailable(data, offset, data.size)
offset += currentRead
val progress = (offset * 100f / data.size).roundToInt()
emit(DownloadResult.Progress(progress))
} while (currentRead > 0)
if (response.status.isSuccess()) {
emit(DownloadResult.Success(data))
} else {
emit(DownloadResult.Error("File not downloaded"))
}
client.close()
}
}
suspend fun downloadFile(url: String): Flow<DownloadResult> {
return flow {
val client = createHttpClient()
@ -65,7 +45,7 @@ suspend fun downloadFile(url: String): Flow<DownloadResult> {
}
fun Dir.cacheImagePostfix():String = "info"
fun Dir.getNameURL(url: String): String {
fun getNameURL(url: String): String {
return url.substring(url.lastIndexOf('/') + 1, url.length)
}
/*

View File

@ -20,6 +20,7 @@ class YoutubeMusic constructor(
val youtubeTracks = mutableListOf<YoutubeTrack>()
val responseObj = Json.parseToJsonElement(getYoutubeMusicResponse(query))
val contentBlocks = responseObj.jsonObject["contents"]
?.jsonObject?.get("sectionListRenderer")
?.jsonObject?.get("contents")?.jsonArray

View File

@ -2,7 +2,11 @@ package com.shabinder.common
import co.touchlab.kermit.Kermit
import com.mpatric.mp3agic.Mp3File
import java.awt.image.BufferedImage
import java.io.*
import java.lang.Exception
import java.net.HttpURLConnection
import java.net.URL
import java.nio.charset.StandardCharsets
import javax.imageio.ImageIO
@ -77,4 +81,69 @@ actual open class Dir actual constructor(private val logger: Kermit) {
.setId3v1Tags(trackDetails)
.setId3v2TagsAndSaveFile(trackDetails,path)
}
actual fun loadImage(url: String, cachePath: String):Picture? {
var picture: Picture? = loadCachedImage(cachePath)
if (picture == null) picture = freshImage(url,cachePath)
return picture
}
private fun loadCachedImage(cachePath: String): Picture? {
return try {
val read = BufferedReader(
InputStreamReader(
FileInputStream(cachePath + cacheImagePostfix()),
StandardCharsets.UTF_8
)
)
val source = read.readLine()
val width = read.readLine().toInt()
val height = read.readLine().toInt()
read.close()
val result: BufferedImage? = ImageIO.read(File(cachePath))
if (result != null) {
Picture(
source,
getNameURL(source),
result,
width,
height
)
}else null
} catch (e: Exception) {
e.printStackTrace()
null
}
}
private fun freshImage(url:String,cachePath: String):Picture?{
return try {
val source = URL(url)
val connection: HttpURLConnection = source.openConnection() as HttpURLConnection
connection.connectTimeout = 5000
connection.connect()
val input: InputStream = connection.inputStream
val result: BufferedImage? = ImageIO.read(input)
if (result != null) {
val picture = Picture(
url,
getNameURL(url),
result,
result.width,
result.height
)
cacheImage(picture)
picture
} else null
} catch (e: Exception) {
e.printStackTrace()
null
}
}
}