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.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.ImageBitmap 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.layout.ContentScale
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.core.net.toUri import androidx.core.net.toUri
import com.shabinder.common.Picture
import com.shabinder.common.database.appContext import com.shabinder.common.database.appContext
import dev.chrisbanes.accompanist.coil.CoilImage 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 @Composable
actual fun ImageLoad( actual fun ImageLoad(
url:String, url:String,
@ -31,6 +49,7 @@ actual fun ImageLoad(
modifier = modifier modifier = modifier
) )
} }
*/
@Composable @Composable
actual fun Toast( actual fun Toast(

View File

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

View File

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

View File

@ -2,6 +2,7 @@ package com.shabinder.common.list.integration
import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.ComponentContext
import com.arkivanov.mvikotlin.extensions.coroutines.states import com.arkivanov.mvikotlin.extensions.coroutines.states
import com.shabinder.common.Picture
import com.shabinder.common.TrackDetails import com.shabinder.common.TrackDetails
import com.shabinder.common.list.SpotiFlyerList import com.shabinder.common.list.SpotiFlyerList
import com.shabinder.common.list.SpotiFlyerList.Dependencies import com.shabinder.common.list.SpotiFlyerList.Dependencies
@ -38,4 +39,6 @@ internal class SpotiFlyerListImpl(
override fun onBackPressed(){ override fun onBackPressed(){
listOutput(SpotiFlyerList.Output.Finished) 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.RouterState
import com.arkivanov.decompose.value.Value import com.arkivanov.decompose.value.Value
import com.arkivanov.mvikotlin.core.store.StoreFactory import com.arkivanov.mvikotlin.core.store.StoreFactory
import com.shabinder.common.Dir
import com.shabinder.common.FetchPlatformQueryResult import com.shabinder.common.FetchPlatformQueryResult
import com.shabinder.common.list.SpotiFlyerList import com.shabinder.common.list.SpotiFlyerList
import com.shabinder.common.main.SpotiFlyerMain import com.shabinder.common.main.SpotiFlyerMain
@ -24,6 +25,7 @@ interface SpotiFlyerRoot {
val storeFactory: StoreFactory val storeFactory: StoreFactory
val database: Database val database: Database
val fetchPlatformQueryResult: FetchPlatformQueryResult 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.Parcelable
import com.arkivanov.decompose.statekeeper.Parcelize import com.arkivanov.decompose.statekeeper.Parcelize
import com.arkivanov.decompose.value.Value import com.arkivanov.decompose.value.Value
import com.shabinder.common.Dir
import com.shabinder.common.list.SpotiFlyerList import com.shabinder.common.list.SpotiFlyerList
import com.shabinder.common.main.SpotiFlyerMain import com.shabinder.common.main.SpotiFlyerMain
import com.shabinder.common.root.SpotiFlyerRoot import com.shabinder.common.root.SpotiFlyerRoot
@ -48,6 +49,7 @@ internal class SpotiFlyerRootImpl(
componentContext = componentContext, componentContext = componentContext,
dependencies = object : SpotiFlyerList.Dependencies, Dependencies by this { dependencies = object : SpotiFlyerList.Dependencies, Dependencies by this {
override val fetchQuery = fetchPlatformQueryResult override val fetchQuery = fetchPlatformQueryResult
override val dir: Dir = directories
override val link: String = link override val link: String = link
override fun listOutput(finished: SpotiFlyerList.Output.Finished): Consumer<SpotiFlyerList.Output> = 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.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import com.shabinder.common.Picture
@Composable @Composable
expect fun ImageLoad( expect fun ImageLoad(
url:String, pic: Picture?,
loadingResource: ImageBitmap? = null,
errorResource: ImageBitmap? = null,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) )

View File

@ -2,14 +2,11 @@ package com.shabinder.common.ui
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap import com.shabinder.common.Picture
import androidx.compose.ui.unit.Dp
@Composable @Composable
actual fun ImageLoad( actual fun ImageLoad(
url:String, pic: Picture?,
loadingResource: ImageBitmap?,
errorResource: ImageBitmap?,
modifier: Modifier 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.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Environment import android.os.Environment
import co.touchlab.kermit.Kermit import co.touchlab.kermit.Kermit
import com.mpatric.mp3agic.Mp3File import com.mpatric.mp3agic.Mp3File
import com.shabinder.common.database.appContext import com.shabinder.common.database.appContext
import java.io.* import java.io.*
import java.lang.Exception
import java.net.HttpURLConnection
import java.net.URL
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
actual open class Dir actual constructor( actual open class Dir actual constructor(
@ -87,4 +91,69 @@ actual open class Dir actual constructor(
.setId3v1Tags(trackDetails) .setId3v1Tags(trackDetails)
.setId3v2TagsAndSaveFile(trackDetails,path) .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 imageCacheDir(): String
fun createDirectory(dirPath:String) fun createDirectory(dirPath:String)
fun cacheImage(picture: Picture) fun cacheImage(picture: Picture)
fun loadImage(url:String, cachePath:String = imageCacheDir() + getNameURL(url)):Picture?
suspend fun clearCache() suspend fun clearCache()
suspend fun saveFileWithMetadata(mp3ByteArray: ByteArray, path: String, trackDetails: TrackDetails) 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> { suspend fun downloadFile(url: String): Flow<DownloadResult> {
return flow { return flow {
val client = createHttpClient() val client = createHttpClient()
@ -65,7 +45,7 @@ suspend fun downloadFile(url: String): Flow<DownloadResult> {
} }
fun Dir.cacheImagePostfix():String = "info" fun Dir.cacheImagePostfix():String = "info"
fun Dir.getNameURL(url: String): String { fun getNameURL(url: String): String {
return url.substring(url.lastIndexOf('/') + 1, url.length) return url.substring(url.lastIndexOf('/') + 1, url.length)
} }
/* /*

View File

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

View File

@ -2,7 +2,11 @@ package com.shabinder.common
import co.touchlab.kermit.Kermit import co.touchlab.kermit.Kermit
import com.mpatric.mp3agic.Mp3File import com.mpatric.mp3agic.Mp3File
import java.awt.image.BufferedImage
import java.io.* import java.io.*
import java.lang.Exception
import java.net.HttpURLConnection
import java.net.URL
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import javax.imageio.ImageIO import javax.imageio.ImageIO
@ -77,4 +81,69 @@ actual open class Dir actual constructor(private val logger: Kermit) {
.setId3v1Tags(trackDetails) .setId3v1Tags(trackDetails)
.setId3v2TagsAndSaveFile(trackDetails,path) .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
}
}
} }