From a7be9567c496906af31a017570e266110d56b74f Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Wed, 16 Oct 2024 13:44:09 -0400 Subject: [PATCH 1/3] Updating how we get images --- androidApp/build.gradle.kts | 4 +- .../com/kevinschildhorn/MainActivity.kt | 30 ++++++-- desktopApp/src/jvmMain/kotlin/Main.kt | 31 +++++++++ gradle/libs.versions.toml | 6 +- shared/build.gradle.kts | 4 +- .../fotopresenter/KoinAndroid.kt | 6 ++ .../ui/shared/SharedImageAndroid.kt | 16 ++++- .../com/kevinschildhorn/fotopresenter/Koin.kt | 9 ++- .../image/CachedImageDataSource.kt | 42 ++++-------- .../data/repositories/ImageRepository.kt | 23 +++++-- .../fotopresenter/ui/ByteArrayFetcher.kt | 22 +++--- .../fotopresenter/ui/shared/SharedCache.kt | 68 +++++++++++++++++-- .../fotopresenter/ui/shared/SharedImage.kt | 4 +- .../ui/shared/SharedImageDesktop.kt | 22 +++++- .../fotopresenter/ui/SMBJFetcher.kt | 28 ++++---- 15 files changed, 227 insertions(+), 88 deletions(-) diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index 1d80406..e6475ce 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -19,7 +19,9 @@ kotlin { implementation(project(":shared")) implementation(libs.koin.android) implementation(libs.firebase.crashlytics) - implementation(libs.coil) + implementation(libs.accompanist.permissions) + implementation(libs.kermit) + } } } diff --git a/androidApp/src/androidMain/kotlin/com/kevinschildhorn/MainActivity.kt b/androidApp/src/androidMain/kotlin/com/kevinschildhorn/MainActivity.kt index 4e9ea81..2cd0d8e 100644 --- a/androidApp/src/androidMain/kotlin/com/kevinschildhorn/MainActivity.kt +++ b/androidApp/src/androidMain/kotlin/com/kevinschildhorn/MainActivity.kt @@ -1,16 +1,25 @@ package com.kevinschildhorn import MainView +import android.Manifest import android.os.Bundle import androidx.activity.compose.setContent +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity +import co.touchlab.kermit.Logger import coil3.ImageLoader import coil3.compose.setSingletonImageLoaderFactory -import com.kevinschildhorn.fotopresenter.data.datasources.image.CachedImageDataSource -import com.kevinschildhorn.fotopresenter.ui.SMBJFetcher -import com.kevinschildhorn.fotopresenter.data.network.NetworkHandler +import coil3.disk.DiskCache +import coil3.disk.directory +import coil3.memory.MemoryCache +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.rememberPermissionState +import com.kevinschildhorn.fotopresenter.baseLogger +import com.kevinschildhorn.fotopresenter.data.network.SMBJHandler import com.kevinschildhorn.fotopresenter.data.repositories.ImageRepository import com.kevinschildhorn.fotopresenter.startKoin +import com.kevinschildhorn.fotopresenter.ui.ByteArrayFetcher +import com.kevinschildhorn.fotopresenter.ui.SMBJFetcher import com.kevinschildhorn.fotopresenter.ui.screens.directory.DirectoryViewModel import com.kevinschildhorn.fotopresenter.ui.screens.login.LoginViewModel import com.kevinschildhorn.fotopresenter.ui.screens.playlist.PlaylistViewModel @@ -32,10 +41,23 @@ class MainActivity : AppCompatActivity(), KoinComponent { startKoin(this) setContent { + setSingletonImageLoaderFactory { context -> ImageLoader.Builder(context) .components { - add(SMBJFetcher.Factory(imageRepository)) + add(SMBJFetcher.Factory(imageRepository, baseLogger)) + add(ByteArrayFetcher.Factory(Logger.withTag("ByteArrayFetcher"))) + } + .memoryCache { + MemoryCache.Builder() + .maxSizePercent(context,0.25) + .build() + } + .diskCache { + DiskCache.Builder() + .directory(context.cacheDir.resolve("image_cache")) + .maxSizePercent(0.02) + .build() } .build() } diff --git a/desktopApp/src/jvmMain/kotlin/Main.kt b/desktopApp/src/jvmMain/kotlin/Main.kt index 44a8bfb..823b05c 100644 --- a/desktopApp/src/jvmMain/kotlin/Main.kt +++ b/desktopApp/src/jvmMain/kotlin/Main.kt @@ -1,14 +1,29 @@ +import KoinPurse.imageRepository import androidx.compose.ui.window.Window import androidx.compose.ui.window.application import co.touchlab.kermit.Logger +import coil3.ImageLoader import com.kevinschildhorn.fotopresenter.UseCaseFactory +import com.kevinschildhorn.fotopresenter.baseLogger +import com.kevinschildhorn.fotopresenter.ui.SMBJFetcher import com.kevinschildhorn.fotopresenter.ui.screens.directory.DirectoryViewModel import com.kevinschildhorn.fotopresenter.ui.screens.login.LoginViewModel import com.kevinschildhorn.fotopresenter.ui.screens.playlist.PlaylistViewModel import com.kevinschildhorn.fotopresenter.ui.screens.slideshow.SlideshowViewModel +import coil3.compose.setSingletonImageLoaderFactory +import com.kevinschildhorn.fotopresenter.data.datasources.image.CachedImageDataSource +import com.kevinschildhorn.fotopresenter.data.datasources.image.NetworkImageDataSource +import com.kevinschildhorn.fotopresenter.data.network.SMBJHandler +import com.kevinschildhorn.fotopresenter.data.repositories.ImageRepository +import com.kevinschildhorn.fotopresenter.ui.ByteArrayFetcher +import com.kevinschildhorn.fotopresenter.ui.shared.SharedFileCache object KoinPurse { + private val remoteImageDataSource: NetworkImageDataSource = NetworkImageDataSource(SMBJHandler) + private val localImageDataSource: CachedImageDataSource = + CachedImageDataSource(SharedFileCache("cache"), Logger.withTag("CachedImageDataSource")) + val loginViewModel = LoginViewModel(Logger.withTag("LoginViewModel"), UseCaseFactory.credentialsRepository) val directoryViewModel = @@ -16,6 +31,12 @@ object KoinPurse { val slideshowViewModel = SlideshowViewModel(Logger.withTag("SlideshowViewModel")) val playlistViewModel = PlaylistViewModel(UseCaseFactory.playlistRepository, Logger.withTag("PlaylistViewModel")) + val imageRepository = + ImageRepository( + remoteImageDataSource, + localImageDataSource, + Logger.withTag("ImageRepository") + ) } fun main() = application { @@ -23,6 +44,16 @@ fun main() = application { title = "FotoPresenter", onCloseRequest = ::exitApplication ) { + + setSingletonImageLoaderFactory { context -> + ImageLoader.Builder(context) + .components { + add(SMBJFetcher.Factory(imageRepository, Logger.withTag("SMBJFetcher"))) + add(ByteArrayFetcher.Factory(Logger.withTag("ByteArrayFetcher"))) + } + .build() + } + MainView( KoinPurse.loginViewModel, KoinPurse.directoryViewModel, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4380afa..d4ee932 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,4 +1,5 @@ [versions] +accompanistPermissions = "0.36.0" activity-compose = "1.9.2" agp = "8.5.2" appcompat = "1.7.0" @@ -7,6 +8,7 @@ cache4k = "0.12.0" coil = "3.0.0-rc01" core-ktx = "1.13.1" eva-icons = "1.1.0" +fileKache = "2.1.0" firebase-crashlytics = "19.2.0" kermit = "2.0.4" kermit-koin = "1.2.2" @@ -33,12 +35,13 @@ sqlite-driver = "2.0.1" turbine = "1.0.0" ## SDK Versions -minSdk = "26" +minSdk = "29" targetSdk = "34" compileSdk = "34" java = "21" [libraries] +accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" } activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" } android-driver = { module = "app.cash.sqldelight:android-driver", version.ref = "sqlite-driver" } appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } @@ -46,6 +49,7 @@ atomik = { module = "io.github.kevinschildhorn:atomik", version.ref = "atomik" } cache4k = { module = "io.github.reactivecircus.cache4k:cache4k", version.ref = "cache4k" } core-ktx = { module = "androidx.core:core-ktx", version.ref = "core-ktx" } eva-icons = { module = "br.com.devsrsouza.compose.icons:eva-icons", version.ref = "eva-icons" } +file-kache = { module = "com.mayakapps.kache:file-kache", version.ref = "fileKache" } firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics", version.ref = "firebase-crashlytics" } kermit = { module = "co.touchlab:kermit", version.ref = "kermit" } kermit-koin = { module = "co.touchlab:kermit-koin", version.ref = "kermit-koin" } diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 10ec4f1..7ba6eca 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -53,8 +53,8 @@ kotlin { implementation(libs.multiplatform.settings) implementation(libs.kotlinx.datetime) implementation(libs.kim) - implementation(libs.coil) - + api(libs.coil) + implementation(libs.file.kache) } } val commonTest by getting { diff --git a/shared/src/androidMain/kotlin/com/kevinschildhorn/fotopresenter/KoinAndroid.kt b/shared/src/androidMain/kotlin/com/kevinschildhorn/fotopresenter/KoinAndroid.kt index b4b5b55..97bc6e8 100644 --- a/shared/src/androidMain/kotlin/com/kevinschildhorn/fotopresenter/KoinAndroid.kt +++ b/shared/src/androidMain/kotlin/com/kevinschildhorn/fotopresenter/KoinAndroid.kt @@ -8,7 +8,9 @@ import app.cash.sqldelight.db.SqlDriver import com.kevinschildhorn.fotopresenter.data.datasources.CredentialsDataSource import com.kevinschildhorn.fotopresenter.data.network.NetworkHandler import com.kevinschildhorn.fotopresenter.data.network.SMBJHandler +import com.kevinschildhorn.fotopresenter.ui.shared.CacheInterface import com.kevinschildhorn.fotopresenter.ui.shared.DriverFactory +import com.kevinschildhorn.fotopresenter.ui.shared.SharedFileCache import com.russhwolf.settings.Settings import com.russhwolf.settings.SharedPreferencesSettings import org.koin.core.KoinApplication @@ -45,6 +47,10 @@ internal actual val platformModule: Module = module { SMBJHandler } single { DriverFactory(context = get()).createDriver() } + single { + val context:Context = get() + SharedFileCache(context.cacheDir.path) + } } @OptIn(KoinInternalApi::class) diff --git a/shared/src/androidMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImageAndroid.kt b/shared/src/androidMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImageAndroid.kt index fd20167..65c4ebc 100644 --- a/shared/src/androidMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImageAndroid.kt +++ b/shared/src/androidMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImageAndroid.kt @@ -4,9 +4,23 @@ import android.graphics.Bitmap import android.graphics.BitmapFactory import coil3.Image import coil3.asImage +import coil3.decode.DataSource +import coil3.fetch.FetchResult +import coil3.fetch.ImageFetchResult actual open class SharedImage actual constructor(actual val byteArray: ByteArray) { - actual fun getCoilImage(size: Int): Image? = getAndroidBitmap(byteArray, size)?.asImage() + + actual fun getFetchResult(size: Int): FetchResult? { + val image: Image? = getAndroidBitmap(byteArray, size)?.asImage() + return if (image != null) ImageFetchResult( + image = image, + isSampled = true, + dataSource = DataSource.NETWORK, + ) + else null + } + + private fun getCoilImage(size: Int): Image? = getAndroidBitmap(byteArray, size)?.asImage() private fun getAndroidBitmap(byteArray: ByteArray, size: Int): Bitmap? { val options = BitmapFactory.Options() diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/Koin.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/Koin.kt index 5a83b91..c719efc 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/Koin.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/Koin.kt @@ -3,10 +3,10 @@ package com.kevinschildhorn.fotopresenter import co.touchlab.kermit.Logger import com.kevinschildhorn.fotopresenter.data.datasources.CredentialsDataSource import com.kevinschildhorn.fotopresenter.data.datasources.DirectoryDataSource -import com.kevinschildhorn.fotopresenter.data.datasources.image.CachedImageDataSource import com.kevinschildhorn.fotopresenter.data.datasources.ImageMetadataDataSource import com.kevinschildhorn.fotopresenter.data.datasources.PlaylistFileDataSource import com.kevinschildhorn.fotopresenter.data.datasources.PlaylistSQLDataSource +import com.kevinschildhorn.fotopresenter.data.datasources.image.CachedImageDataSource import com.kevinschildhorn.fotopresenter.data.datasources.image.NetworkImageDataSource import com.kevinschildhorn.fotopresenter.data.repositories.CredentialsRepository import com.kevinschildhorn.fotopresenter.data.repositories.DirectoryRepository @@ -27,7 +27,7 @@ import com.kevinschildhorn.fotopresenter.ui.screens.login.LoginViewModel import com.kevinschildhorn.fotopresenter.ui.screens.playlist.PlaylistViewModel import com.kevinschildhorn.fotopresenter.ui.screens.slideshow.SlideshowViewModel import com.kevinschildhorn.fotopresenter.ui.shared.CacheInterface -import com.kevinschildhorn.fotopresenter.ui.shared.SharedInMemoryCache +import com.kevinschildhorn.fotopresenter.ui.shared.SharedFileCache import org.koin.core.module.Module import org.koin.dsl.module @@ -39,13 +39,12 @@ val commonModule = module { // Data - single { SharedInMemoryCache } single { NetworkImageDataSource(get()) } single { CredentialsDataSource(get()) } single { CredentialsRepository(get()) } single { DirectoryDataSource(get(), baseLogger.withTag("DirectoryDataSource")) } single { DirectoryRepository(get(), get()) } - single { CachedImageDataSource(get(), get(), baseLogger.withTag("ImageCacheDataSource")) } + single { CachedImageDataSource(get(), baseLogger.withTag("ImageCacheDataSource"), get()) } single { PlaylistFileDataSource(baseLogger.withTag("PlaylistDataSource"), get()) } single { PlaylistSQLDataSource(get(), baseLogger.withTag("PlaylistDataSource")) } single { PlaylistRepository(get(), get()) } @@ -77,7 +76,7 @@ val commonModule = baseLogger.withTag("RetrieveDirectoryContentsUseCase"), ) } - factory { RetrieveImageUseCase(get(), baseLogger.withTag("RetrieveImagesUseCase")) } + factory { RetrieveImageUseCase(get(), baseLogger.withTag("RetrieveImageUseCase")) } factory { SaveMetadataForPathUseCase(get()) } // UI single { LoginViewModel(baseLogger.withTag("LoginViewModel"), get()) } diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/image/CachedImageDataSource.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/image/CachedImageDataSource.kt index 3988dda..98ed4bd 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/image/CachedImageDataSource.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/image/CachedImageDataSource.kt @@ -1,60 +1,42 @@ package com.kevinschildhorn.fotopresenter.data.datasources.image -import androidx.collection.size import app.cash.sqldelight.db.SqlDriver import co.touchlab.kermit.Logger -import com.kevinschildhorn.fotopresenter.Image as SQLImage import com.kevinschildhorn.fotopresenter.PlaylistDatabase import com.kevinschildhorn.fotopresenter.data.network.NetworkDirectoryDetails import com.kevinschildhorn.fotopresenter.ui.shared.CacheInterface import com.kevinschildhorn.fotopresenter.ui.shared.SharedImage -import java.io.InputStream class CachedImageDataSource( private val cache: CacheInterface, + private val logger: Logger, driver: SqlDriver, - private val logger: Logger ) { private val database = PlaylistDatabase(driver) - fun getImage(directory: NetworkDirectoryDetails): SharedImage? { + suspend fun getImage(directory: NetworkDirectoryDetails): SharedImage? { logger.i { "Getting Image from Cache ${directory.cacheId}" } return try { - val image: SQLImage = database.imageQueries.selectImageByName(directory.cacheId).executeAsOne() - SharedImage(image.image) + cache.getImage(directory.cacheId) + //val image = database.imageQueries.selectImageByName(directory.cacheId).executeAsOne() + //SharedImage(image.image) } catch (e: Exception) { logger.e(e) { "Image NOT found" } + logger.e { e.localizedMessage ?: "" } null } } - fun saveImage(directory: NetworkDirectoryDetails, image: SharedImage) { + suspend fun saveImage(directory: NetworkDirectoryDetails, image: SharedImage) { logger.i { "Saving Image To Cache ${directory.cacheId}" } - database.imageQueries.insertImage( - directory.cacheId, - image.byteArray, - ) + cache.cacheImage(directory.cacheId, image) + //database.imageQueries.insertImage( + // directory.cacheId, + // image.byteArray, + //) logger.i { "Image Saved" } - //cache.cacheImage(directory.cacheId, image) TODO } private val NetworkDirectoryDetails.cacheId: String get() = "$name.$id" - - - fun readBlobInChunks(cursor: Cursor, columnIndex: Int): InputStream { - return object : InputStream() { - private var pos = 0 - private val blob = cursor.getBlob(columnIndex) - - override fun read(): Int { - if (pos >= blob.size) { - return -1 - } - return blob[pos++].toInt() and 0xFF - } - - // Implement other methods of InputStream if needed - } - } } diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/repositories/ImageRepository.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/repositories/ImageRepository.kt index afb06f1..b5ed6bb 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/repositories/ImageRepository.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/repositories/ImageRepository.kt @@ -1,10 +1,11 @@ package com.kevinschildhorn.fotopresenter.data.repositories import co.touchlab.kermit.Logger -import coil3.Image +import coil3.fetch.FetchResult import com.kevinschildhorn.fotopresenter.data.datasources.image.CachedImageDataSource import com.kevinschildhorn.fotopresenter.data.datasources.image.NetworkImageDataSource import com.kevinschildhorn.fotopresenter.data.network.NetworkDirectoryDetails +import com.kevinschildhorn.fotopresenter.ui.shared.SharedImage class ImageRepository( private val remoteImageDataSource: NetworkImageDataSource, @@ -12,17 +13,25 @@ class ImageRepository( private val logger: Logger?, ) { - suspend fun getCoilImage(directoryDetails: NetworkDirectoryDetails, size: Int): Image? { - logger?.i { "Getting Image from Cache" } + suspend fun getFetchResult(directoryDetails: NetworkDirectoryDetails, size: Int): FetchResult? { + val image = getImage(directoryDetails, size) + return image?.getFetchResult(size) + } + + private suspend fun getImage(directoryDetails: NetworkDirectoryDetails, size: Int): SharedImage? { + logger?.i { "Getting Image from Cache: ${directoryDetails.name}" } val cachedImage = localImageDataSource.getImage(directoryDetails) - if (cachedImage != null) return cachedImage.getCoilImage(size) + if (cachedImage != null) { + logger?.i { "Cached image found from Cache: ${directoryDetails.name}" } + return cachedImage + } logger?.i { "No cached image found, getting image from directory" } val image = remoteImageDataSource.getImage(directoryDetails) - if(image != null) { - logger?.i { "Storing image in cache" } + if (image != null) { + logger?.i { "Storing image in cache: ${directoryDetails.name}" } localImageDataSource.saveImage(directoryDetails, image) } - return image?.getCoilImage(size) + return image } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/ByteArrayFetcher.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/ByteArrayFetcher.kt index 070daf5..e5a93df 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/ByteArrayFetcher.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/ByteArrayFetcher.kt @@ -1,10 +1,10 @@ package com.kevinschildhorn.fotopresenter.ui +import co.touchlab.kermit.Logger import coil3.ImageLoader import coil3.decode.DataSource import coil3.fetch.FetchResult import coil3.fetch.Fetcher -import coil3.fetch.ImageFetchResult import coil3.request.Options import com.kevinschildhorn.fotopresenter.data.network.NetworkHandler import kotlinx.coroutines.Dispatchers @@ -13,30 +13,28 @@ import com.kevinschildhorn.fotopresenter.ui.shared.SharedImage class ByteArrayFetcher( private val byteArray: ByteArray, - private val networkHandler: NetworkHandler, + private val logger: Logger, ) : Fetcher { override suspend fun fetch(): FetchResult? { return withContext(Dispatchers.IO) { val image = SharedImage(byteArray) - val coilImage = image.getCoilImage(64) - if (coilImage != null) { - ImageFetchResult( - image = coilImage, - isSampled = true, - dataSource = DataSource.NETWORK, - ) + val result = image.getFetchResult(64) + if (result != null) { + logger.i { "Image Got!" } + result } else { - throw Exception("Failed to fetch image from FTP") + logger.i { "No Image Fetched" } + null } } } - class Factory(private val networkHandler: NetworkHandler) : Fetcher.Factory { + class Factory(private val logger: Logger) : Fetcher.Factory { override fun create( data: ByteArray, options: Options, imageLoader: ImageLoader - ): Fetcher = ByteArrayFetcher(data, networkHandler) + ): Fetcher = ByteArrayFetcher(data, logger) } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedCache.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedCache.kt index 52c02f1..330b60a 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedCache.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedCache.kt @@ -1,11 +1,14 @@ package com.kevinschildhorn.fotopresenter.ui.shared +import com.mayakapps.kache.FileKache +import com.mayakapps.kache.KacheStrategy import io.github.reactivecircus.cache4k.Cache +import java.io.File interface CacheInterface { - fun getImage(id: String): SharedImage? + suspend fun getImage(id: String): SharedImage? - fun cacheImage( + suspend fun cacheImage( id: String, image: SharedImage, ) @@ -14,11 +17,11 @@ interface CacheInterface { object SharedInMemoryCache : CacheInterface { private val imageCache = Cache.Builder().build() - override fun getImage(id: String): SharedImage? = imageCache.get(id)?.let { + override suspend fun getImage(id: String): SharedImage? = imageCache.get(id)?.let { SharedImage(it) } - override fun cacheImage( + override suspend fun cacheImage( id: String, image: SharedImage, ) { @@ -26,17 +29,68 @@ object SharedInMemoryCache : CacheInterface { } } +class SharedFileCache(private val cacheLocation: String) : CacheInterface { + + override suspend fun getImage(id: String): SharedImage? { + val cache = createCache() + + val test = cache?.get(id) + println(test) + val byteArray = cache?.get(id)?.toByteArray() + return if (byteArray != null) SharedImage(byteArray) else null + } + + override suspend fun cacheImage( + id: String, + image: SharedImage, + ) { + val cache = createCache() + try { + val imageData = cache?.put(id) { path -> + val file = File(path) + try { + val stream = file.outputStream() + stream.write(image.byteArray) + stream.close() + true + } catch (e: Exception) { + println(e.localizedMessage) + false + } + } + println(imageData) + } finally { + cache?.close() + } + } + + + private suspend fun createCache(): FileKache? { + try { + println("Creating Cache") + return FileKache(directory = cacheLocation, maxSize = 10 * 1024 * 1024) { + strategy = KacheStrategy.LRU + + } + } catch (e: Exception) { + println("Error Creating Cache") + println(e.localizedMessage) + return null + } + } +} + class MockSharedCache : CacheInterface { private val contents = mutableMapOf() - override fun cacheImage( + override suspend fun cacheImage( id: String, image: SharedImage, ) { contents[id] = image.byteArray } - override fun getImage(id: String): SharedImage? = contents[id]?.let { + override suspend fun getImage(id: String): SharedImage? = contents[id]?.let { SharedImage(it) } -} +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImage.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImage.kt index 2ed8c1b..30ec161 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImage.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImage.kt @@ -1,11 +1,11 @@ package com.kevinschildhorn.fotopresenter.ui.shared -import coil3.Image +import coil3.fetch.FetchResult expect class SharedImage(byteArray: ByteArray) { val byteArray: ByteArray - fun getCoilImage(size: Int): Image? + fun getFetchResult(size: Int): FetchResult? } fun getScaledDimensions( diff --git a/shared/src/desktopMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImageDesktop.kt b/shared/src/desktopMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImageDesktop.kt index f0aca7c..0bc46fc 100644 --- a/shared/src/desktopMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImageDesktop.kt +++ b/shared/src/desktopMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImageDesktop.kt @@ -1,10 +1,28 @@ package com.kevinschildhorn.fotopresenter.ui.shared import coil3.Image +import coil3.decode.DataSource +import coil3.decode.ImageSource +import coil3.fetch.FetchResult +import coil3.fetch.SourceFetchResult +import okio.FileSystem +import okio.buffer +import okio.source +import java.io.ByteArrayInputStream actual open class SharedImage actual constructor(actual val byteArray: ByteArray) { - // TODO - actual fun getCoilImage(size: Int): Image? = null + actual fun getFetchResult(size: Int): FetchResult? { + val source = ByteArrayInputStream(byteArray).source().buffer() + + return SourceFetchResult( + source = ImageSource( + source = source, + fileSystem = FileSystem.SYSTEM, + ), + mimeType = null, + dataSource = DataSource.NETWORK, + ) + } } diff --git a/shared/src/jvmMain/kotlin/com/kevinschildhorn/fotopresenter/ui/SMBJFetcher.kt b/shared/src/jvmMain/kotlin/com/kevinschildhorn/fotopresenter/ui/SMBJFetcher.kt index ba19b31..10403a1 100644 --- a/shared/src/jvmMain/kotlin/com/kevinschildhorn/fotopresenter/ui/SMBJFetcher.kt +++ b/shared/src/jvmMain/kotlin/com/kevinschildhorn/fotopresenter/ui/SMBJFetcher.kt @@ -1,5 +1,6 @@ package com.kevinschildhorn.fotopresenter.ui +import co.touchlab.kermit.Logger import coil3.ImageLoader import coil3.decode.DataSource import coil3.fetch.FetchResult @@ -15,29 +16,28 @@ import kotlinx.coroutines.withContext class SMBJFetcher( private val directoryDetails: NetworkDirectoryDetails, private val imageRepository: ImageRepository, + private val logger: Logger, ) : Fetcher { - override suspend fun fetch(): FetchResult? { - return withContext(Dispatchers.IO) { - val image = imageRepository.getCoilImage(directoryDetails, 64) - if(image != null) { - ImageFetchResult( - image = image, - isSampled = true, - dataSource = DataSource.NETWORK, - ) - } else { - null - } + override suspend fun fetch(): FetchResult? = withContext(Dispatchers.IO) { + logger.i { "Fetching image: ${directoryDetails.name}" } + + val image = imageRepository.getFetchResult(directoryDetails, 64) + if (image != null) { + logger.i { "Image Got! ${directoryDetails.name}" } + image + } else { + logger.i { "No Image Fetched: ${directoryDetails.name}" } + null } } - class Factory(private val imageRepository: ImageRepository) : + class Factory(private val imageRepository: ImageRepository, private val logger: Logger) : Fetcher.Factory { override fun create( data: NetworkDirectoryDetails, options: Options, imageLoader: ImageLoader - ): Fetcher = SMBJFetcher(data, imageRepository) + ): Fetcher = SMBJFetcher(data, imageRepository, logger) } } \ No newline at end of file From 25134be92b610ba0a9c7682fa7865ea3d720a211 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Wed, 16 Oct 2024 15:59:38 -0400 Subject: [PATCH 2/3] formatting --- .../com/kevinschildhorn/fotopresenter/Koin.kt | 2 - .../image/CachedImageDataSource.kt | 9 ++-- .../data/repositories/ImageRepository.kt | 11 +++-- .../fotopresenter/ui/ByteArrayFetcher.kt | 4 +- .../fotopresenter/ui/shared/SharedCache.kt | 42 +++++++++---------- .../fotopresenter/ui/shared/SharedImage.kt | 1 + .../ui/shared/SharedImageDesktop.kt | 10 ++--- .../fotopresenter/ui/SMBJFetcher.kt | 26 ++++++------ 8 files changed, 52 insertions(+), 53 deletions(-) diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/Koin.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/Koin.kt index 0e21306..826192e 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/Koin.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/Koin.kt @@ -26,8 +26,6 @@ import com.kevinschildhorn.fotopresenter.ui.screens.directory.DirectoryViewModel import com.kevinschildhorn.fotopresenter.ui.screens.login.LoginViewModel import com.kevinschildhorn.fotopresenter.ui.screens.playlist.PlaylistViewModel import com.kevinschildhorn.fotopresenter.ui.screens.slideshow.SlideshowViewModel -import com.kevinschildhorn.fotopresenter.ui.shared.CacheInterface -import com.kevinschildhorn.fotopresenter.ui.shared.SharedFileCache import org.koin.core.module.Module import org.koin.dsl.module diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/image/CachedImageDataSource.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/image/CachedImageDataSource.kt index 14f3f9f..739a112 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/image/CachedImageDataSource.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/image/CachedImageDataSource.kt @@ -6,7 +6,6 @@ import com.kevinschildhorn.fotopresenter.PlaylistDatabase import com.kevinschildhorn.fotopresenter.data.network.NetworkDirectoryDetails import com.kevinschildhorn.fotopresenter.ui.shared.CacheInterface import com.kevinschildhorn.fotopresenter.ui.shared.SharedImage -import com.kevinschildhorn.fotopresenter.Image as SQLImage class CachedImageDataSource( private val cache: CacheInterface, @@ -20,8 +19,8 @@ class CachedImageDataSource( logger.i { "Getting Image from Cache ${directory.cacheId}" } return try { cache.getImage(directory.cacheId) - //val image = database.imageQueries.selectImageByName(directory.cacheId).executeAsOne() - //SharedImage(image.image) + // val image = database.imageQueries.selectImageByName(directory.cacheId).executeAsOne() + // SharedImage(image.image) } catch (e: Exception) { logger.e(e) { "Image NOT found" } logger.e { e.localizedMessage ?: "" } @@ -35,10 +34,10 @@ class CachedImageDataSource( ) { logger.i { "Saving Image To Cache ${directory.cacheId}" } cache.cacheImage(directory.cacheId, image) - //database.imageQueries.insertImage( + // database.imageQueries.insertImage( // directory.cacheId, // image.byteArray, - //) + // ) logger.i { "Image Saved" } // cache.cacheImage(directory.cacheId, image) TODO } diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/repositories/ImageRepository.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/repositories/ImageRepository.kt index e31d4db..af52436 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/repositories/ImageRepository.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/repositories/ImageRepository.kt @@ -12,13 +12,18 @@ class ImageRepository( private val localImageDataSource: CachedImageDataSource, private val logger: Logger?, ) { - - suspend fun getFetchResult(directoryDetails: NetworkDirectoryDetails, size: Int): FetchResult? { + suspend fun getFetchResult( + directoryDetails: NetworkDirectoryDetails, + size: Int, + ): FetchResult? { val image = getImage(directoryDetails, size) return image?.getFetchResult(size) } - private suspend fun getImage(directoryDetails: NetworkDirectoryDetails, size: Int): SharedImage? { + private suspend fun getImage( + directoryDetails: NetworkDirectoryDetails, + size: Int, + ): SharedImage? { logger?.i { "Getting Image from Cache: ${directoryDetails.name}" } val cachedImage = localImageDataSource.getImage(directoryDetails) if (cachedImage != null) { diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/ByteArrayFetcher.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/ByteArrayFetcher.kt index 1c05013..6ce5ce2 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/ByteArrayFetcher.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/ByteArrayFetcher.kt @@ -2,11 +2,9 @@ package com.kevinschildhorn.fotopresenter.ui import co.touchlab.kermit.Logger import coil3.ImageLoader -import coil3.decode.DataSource import coil3.fetch.FetchResult import coil3.fetch.Fetcher import coil3.request.Options -import com.kevinschildhorn.fotopresenter.data.network.NetworkHandler import com.kevinschildhorn.fotopresenter.ui.shared.SharedImage import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -33,7 +31,7 @@ class ByteArrayFetcher( override fun create( data: ByteArray, options: Options, - imageLoader: ImageLoader + imageLoader: ImageLoader, ): Fetcher = ByteArrayFetcher(data, logger) } } diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedCache.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedCache.kt index a8b7df2..891ef2e 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedCache.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedCache.kt @@ -17,9 +17,10 @@ interface CacheInterface { object SharedInMemoryCache : CacheInterface { private val imageCache = Cache.Builder().build() - override suspend fun getImage(id: String): SharedImage? = imageCache.get(id)?.let { - SharedImage(it) - } + override suspend fun getImage(id: String): SharedImage? = + imageCache.get(id)?.let { + SharedImage(it) + } override suspend fun cacheImage( id: String, @@ -30,7 +31,6 @@ object SharedInMemoryCache : CacheInterface { } class SharedFileCache(private val cacheLocation: String) : CacheInterface { - override suspend fun getImage(id: String): SharedImage? { val cache = createCache() @@ -46,31 +46,30 @@ class SharedFileCache(private val cacheLocation: String) : CacheInterface { ) { val cache = createCache() try { - val imageData = cache?.put(id) { path -> - val file = File(path) - try { - val stream = file.outputStream() - stream.write(image.byteArray) - stream.close() - true - } catch (e: Exception) { - println(e.localizedMessage) - false + val imageData = + cache?.put(id) { path -> + val file = File(path) + try { + val stream = file.outputStream() + stream.write(image.byteArray) + stream.close() + true + } catch (e: Exception) { + println(e.localizedMessage) + false + } } - } println(imageData) } finally { cache?.close() } } - private suspend fun createCache(): FileKache? { try { println("Creating Cache") - return FileKache(directory = cacheLocation, maxSize = 10 * 1024 * 1024) { + return FileKache(directory = cacheLocation, maxSize = 10 * 1024 * 1024) { strategy = KacheStrategy.LRU - } } catch (e: Exception) { println("Error Creating Cache") @@ -90,7 +89,8 @@ class MockSharedCache : CacheInterface { contents[id] = image.byteArray } - override suspend fun getImage(id: String): SharedImage? = contents[id]?.let { - SharedImage(it) - } + override suspend fun getImage(id: String): SharedImage? = + contents[id]?.let { + SharedImage(it) + } } diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImage.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImage.kt index bea3c18..c229a9b 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImage.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImage.kt @@ -4,6 +4,7 @@ import coil3.fetch.FetchResult expect class SharedImage(byteArray: ByteArray) { val byteArray: ByteArray + fun getFetchResult(size: Int): FetchResult? } diff --git a/shared/src/desktopMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImageDesktop.kt b/shared/src/desktopMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImageDesktop.kt index 4c22a3e..e492b0a 100644 --- a/shared/src/desktopMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImageDesktop.kt +++ b/shared/src/desktopMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImageDesktop.kt @@ -2,7 +2,6 @@ package com.kevinschildhorn.fotopresenter.ui.shared -import coil3.Image import coil3.decode.DataSource import coil3.decode.ImageSource import coil3.fetch.FetchResult @@ -17,10 +16,11 @@ actual open class SharedImage actual constructor(actual val byteArray: ByteArray val source = ByteArrayInputStream(byteArray).source().buffer() return SourceFetchResult( - source = ImageSource( - source = source, - fileSystem = FileSystem.SYSTEM, - ), + source = + ImageSource( + source = source, + fileSystem = FileSystem.SYSTEM, + ), mimeType = null, dataSource = DataSource.NETWORK, ) diff --git a/shared/src/jvmMain/kotlin/com/kevinschildhorn/fotopresenter/ui/SMBJFetcher.kt b/shared/src/jvmMain/kotlin/com/kevinschildhorn/fotopresenter/ui/SMBJFetcher.kt index 4add029..e1caacb 100644 --- a/shared/src/jvmMain/kotlin/com/kevinschildhorn/fotopresenter/ui/SMBJFetcher.kt +++ b/shared/src/jvmMain/kotlin/com/kevinschildhorn/fotopresenter/ui/SMBJFetcher.kt @@ -2,10 +2,8 @@ package com.kevinschildhorn.fotopresenter.ui import co.touchlab.kermit.Logger import coil3.ImageLoader -import coil3.decode.DataSource import coil3.fetch.FetchResult import coil3.fetch.Fetcher -import coil3.fetch.ImageFetchResult import coil3.request.Options import com.kevinschildhorn.fotopresenter.data.network.NetworkDirectoryDetails import com.kevinschildhorn.fotopresenter.data.repositories.ImageRepository @@ -17,26 +15,26 @@ class SMBJFetcher( private val imageRepository: ImageRepository, private val logger: Logger, ) : Fetcher { + override suspend fun fetch(): FetchResult? = + withContext(Dispatchers.IO) { + logger.i { "Fetching image: ${directoryDetails.name}" } - override suspend fun fetch(): FetchResult? = withContext(Dispatchers.IO) { - logger.i { "Fetching image: ${directoryDetails.name}" } - - val image = imageRepository.getFetchResult(directoryDetails, 64) - if (image != null) { - logger.i { "Image Got! ${directoryDetails.name}" } - image - } else { - logger.i { "No Image Fetched: ${directoryDetails.name}" } - null + val image = imageRepository.getFetchResult(directoryDetails, 64) + if (image != null) { + logger.i { "Image Got! ${directoryDetails.name}" } + image + } else { + logger.i { "No Image Fetched: ${directoryDetails.name}" } + null + } } - } class Factory(private val imageRepository: ImageRepository, private val logger: Logger) : Fetcher.Factory { override fun create( data: NetworkDirectoryDetails, options: Options, - imageLoader: ImageLoader + imageLoader: ImageLoader, ): Fetcher = SMBJFetcher(data, imageRepository, logger) } } From 2f67ae93e6ba61f1631479239e58a71589707807 Mon Sep 17 00:00:00 2001 From: Kevin Schildhorn Date: Wed, 16 Oct 2024 16:03:21 -0400 Subject: [PATCH 3/3] formatting --- .../fotopresenter/KoinAndroid.kt | 17 ++++++++--------- .../ui/shared/SharedImageAndroid.kt | 16 +++++++++------- .../datasources/image/CachedImageDataSource.kt | 3 +-- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/shared/src/androidMain/kotlin/com/kevinschildhorn/fotopresenter/KoinAndroid.kt b/shared/src/androidMain/kotlin/com/kevinschildhorn/fotopresenter/KoinAndroid.kt index d474922..181713f 100644 --- a/shared/src/androidMain/kotlin/com/kevinschildhorn/fotopresenter/KoinAndroid.kt +++ b/shared/src/androidMain/kotlin/com/kevinschildhorn/fotopresenter/KoinAndroid.kt @@ -49,16 +49,15 @@ internal actual val platformModule: Module = SMBJHandler } single { DriverFactory(context = get()).createDriver() } + single { + SMBJHandler + } + single { DriverFactory(context = get()).createDriver() } + single { + val context: Context = get() + SharedFileCache(context.cacheDir.path) + } } - single { - SMBJHandler - } - single { DriverFactory(context = get()).createDriver() } - single { - val context:Context = get() - SharedFileCache(context.cacheDir.path) - } -} @OptIn(KoinInternalApi::class) fun KoinApplication.androidContext(androidContext: Context): KoinApplication { diff --git a/shared/src/androidMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImageAndroid.kt b/shared/src/androidMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImageAndroid.kt index 7ac17a6..c3fa9a6 100644 --- a/shared/src/androidMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImageAndroid.kt +++ b/shared/src/androidMain/kotlin/com/kevinschildhorn/fotopresenter/ui/shared/SharedImageAndroid.kt @@ -11,15 +11,17 @@ import coil3.fetch.FetchResult import coil3.fetch.ImageFetchResult actual open class SharedImage actual constructor(actual val byteArray: ByteArray) { - actual fun getFetchResult(size: Int): FetchResult? { val image: Image? = getAndroidBitmap(byteArray, size)?.asImage() - return if (image != null) ImageFetchResult( - image = image, - isSampled = true, - dataSource = DataSource.NETWORK, - ) - else null + return if (image != null) { + ImageFetchResult( + image = image, + isSampled = true, + dataSource = DataSource.NETWORK, + ) + } else { + null + } } private fun getCoilImage(size: Int): Image? = getAndroidBitmap(byteArray, size)?.asImage() diff --git a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/image/CachedImageDataSource.kt b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/image/CachedImageDataSource.kt index 739a112..ddb6791 100644 --- a/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/image/CachedImageDataSource.kt +++ b/shared/src/commonMain/kotlin/com/kevinschildhorn/fotopresenter/data/datasources/image/CachedImageDataSource.kt @@ -11,7 +11,6 @@ class CachedImageDataSource( private val cache: CacheInterface, private val logger: Logger, driver: SqlDriver, - private val logger: Logger, ) { private val database = PlaylistDatabase(driver) @@ -28,7 +27,7 @@ class CachedImageDataSource( } } - fun saveImage( + suspend fun saveImage( directory: NetworkDirectoryDetails, image: SharedImage, ) {