mirror of
https://github.com/Shabinder/SpotiFlyer.git
synced 2024-11-22 09:04:32 +01:00
Image caching
This commit is contained in:
parent
3ae3b404b1
commit
e63a5d97fd
@ -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(
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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> =
|
||||
|
@ -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
|
||||
)
|
||||
|
||||
|
@ -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
|
||||
){
|
||||
|
||||
|
21
common/compose-ui/src/main/res/drawable/music.xml
Normal file
21
common/compose-ui/src/main/res/drawable/music.xml
Normal 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>
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
/*
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user