diff --git a/common/compose-ui/src/androidMain/kotlin/com/shabinder/common/ui/Actual.kt b/common/compose-ui/src/androidMain/kotlin/com/shabinder/common/ui/Actual.kt index 4d3755ff..01f0f941 100644 --- a/common/compose-ui/src/androidMain/kotlin/com/shabinder/common/ui/Actual.kt +++ b/common/compose-ui/src/androidMain/kotlin/com/shabinder/common/ui/Actual.kt @@ -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( diff --git a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerList.kt b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerList.kt index b82ea4b1..18c6817f 100644 --- a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerList.kt +++ b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerList.kt @@ -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 } diff --git a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerListUi.kt b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerListUi.kt index 85648587..28eef9ed 100644 --- a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerListUi.kt +++ b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerListUi.kt @@ -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) diff --git a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/integration/SpotiFlyerListImpl.kt b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/integration/SpotiFlyerListImpl.kt index 4f596124..4f01d885 100644 --- a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/integration/SpotiFlyerListImpl.kt +++ b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/list/integration/SpotiFlyerListImpl.kt @@ -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) } \ No newline at end of file diff --git a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/root/SpotiFlyerRoot.kt b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/root/SpotiFlyerRoot.kt index 636c52e9..162983ea 100644 --- a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/root/SpotiFlyerRoot.kt +++ b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/root/SpotiFlyerRoot.kt @@ -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 } } diff --git a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/root/integration/SpotiFlyerRootImpl.kt b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/root/integration/SpotiFlyerRootImpl.kt index 4c1503fb..4855f46a 100644 --- a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/root/integration/SpotiFlyerRootImpl.kt +++ b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/root/integration/SpotiFlyerRootImpl.kt @@ -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 = diff --git a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/Expect.kt b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/Expect.kt index 2ff87db1..11db9122 100644 --- a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/Expect.kt +++ b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/Expect.kt @@ -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 ) diff --git a/common/compose-ui/src/desktopMain/kotlin/com/shabinder/common/ui/Actual.kt b/common/compose-ui/src/desktopMain/kotlin/com/shabinder/common/ui/Actual.kt index e92f72d0..aa951174 100644 --- a/common/compose-ui/src/desktopMain/kotlin/com/shabinder/common/ui/Actual.kt +++ b/common/compose-ui/src/desktopMain/kotlin/com/shabinder/common/ui/Actual.kt @@ -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 ){ diff --git a/common/compose-ui/src/main/res/drawable/music.xml b/common/compose-ui/src/main/res/drawable/music.xml new file mode 100644 index 00000000..04a9c803 --- /dev/null +++ b/common/compose-ui/src/main/res/drawable/music.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/Dir.kt b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/Dir.kt index 15ac243d..d0f6a882 100644 --- a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/Dir.kt +++ b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/Dir.kt @@ -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 + } + } } \ No newline at end of file diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/Dir.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/Dir.kt index 63bccc6f..4035cb1e 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/Dir.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/Dir.kt @@ -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 { - return flow { - val client = createHttpClient() - val response = client.get(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 { return flow { val client = createHttpClient() @@ -65,7 +45,7 @@ suspend fun downloadFile(url: String): Flow { } fun Dir.cacheImagePostfix():String = "info" -fun Dir.getNameURL(url: String): String { +fun getNameURL(url: String): String { return url.substring(url.lastIndexOf('/') + 1, url.length) } /* diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/providers/YoutubeMusic.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/providers/YoutubeMusic.kt index b33dcc46..d319d18c 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/providers/YoutubeMusic.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/providers/YoutubeMusic.kt @@ -20,6 +20,7 @@ class YoutubeMusic constructor( val youtubeTracks = mutableListOf() val responseObj = Json.parseToJsonElement(getYoutubeMusicResponse(query)) + val contentBlocks = responseObj.jsonObject["contents"] ?.jsonObject?.get("sectionListRenderer") ?.jsonObject?.get("contents")?.jsonArray diff --git a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/Dir.kt b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/Dir.kt index 74837340..3ddccbe4 100644 --- a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/Dir.kt +++ b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/Dir.kt @@ -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 + } + } }