diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 09d4300..b242aea 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -28,12 +28,7 @@ kotlin { } wasmJs { - moduleName = "composeApp" - browser { - commonWebpackConfig { - outputFileName = "composeApp.js" - } - } + browser() binaries.executable() } @@ -78,9 +73,9 @@ kotlin { implementation(libs.kotlinx.serialization.json) implementation(libs.bundles.ktor.common) implementation(libs.bundles.koin.common) - implementation(libs.bundles.coil.common) implementation(libs.napier) implementation(libs.windowSizeClass) + api("io.github.qdsfdhvh:image-loader:1.9.0") } desktopMain.dependencies { implementation(compose.desktop.currentOs) diff --git a/composeApp/src/androidMain/kotlin/imageLoader/ImageLoader.android.kt b/composeApp/src/androidMain/kotlin/imageLoader/ImageLoader.android.kt new file mode 100644 index 0000000..80a905f --- /dev/null +++ b/composeApp/src/androidMain/kotlin/imageLoader/ImageLoader.android.kt @@ -0,0 +1,28 @@ +package imageLoader + +import applicationContext +import com.seiko.imageloader.ImageLoader +import com.seiko.imageloader.component.setupDefaultComponents +import com.seiko.imageloader.intercept.painterMemoryCacheConfig +import com.seiko.imageloader.option.androidContext +import okio.Path.Companion.toOkioPath + +actual fun generateImageLoader(): ImageLoader { + return ImageLoader { + options { + androidContext(applicationContext) + } + components { + setupDefaultComponents() + } + interceptor { + painterMemoryCacheConfig { + maxSize(50) + } + diskCacheConfig { + directory(applicationContext.cacheDir.resolve("image_cache").toOkioPath()) + maxSizeBytes(512L * 1024 * 1024) // 512MB + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/App.kt b/composeApp/src/commonMain/kotlin/App.kt index d45a577..9945b10 100644 --- a/composeApp/src/commonMain/kotlin/App.kt +++ b/composeApp/src/commonMain/kotlin/App.kt @@ -5,20 +5,17 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.navigation.compose.rememberNavController -import coil3.annotation.ExperimentalCoilApi -import coil3.asyncImageLoader -import coil3.compose.setSingletonImageLoaderFactory -import coil3.enableDiskCache +import com.seiko.imageloader.LocalImageLoader +import imageLoader.generateImageLoader import navigation.Navigation import org.jetbrains.compose.ui.tooling.preview.Preview import org.koin.compose.KoinApplication import planets.di.planetsModule import theme.PlanetsKMPTheme -@OptIn(ExperimentalCoilApi::class) @Composable @Preview -fun App(disableDiskCache: Boolean = false) = PlanetsKMPTheme { +fun App() = PlanetsKMPTheme { val navController = rememberNavController() @@ -30,20 +27,18 @@ fun App(disableDiskCache: Boolean = false) = PlanetsKMPTheme { ) } ){ - - setSingletonImageLoaderFactory { context -> - if (disableDiskCache) context.asyncImageLoader() else - context.asyncImageLoader().enableDiskCache() - } - - Box( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background) + CompositionLocalProvider( + LocalImageLoader provides remember { generateImageLoader() }, ) { - Navigation( - navController = navController - ) + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + ) { + Navigation( + navController = navController + ) + } } } diff --git a/composeApp/src/commonMain/kotlin/coil3/ImageLoader.kt b/composeApp/src/commonMain/kotlin/coil3/ImageLoader.kt deleted file mode 100644 index 6afc64a..0000000 --- a/composeApp/src/commonMain/kotlin/coil3/ImageLoader.kt +++ /dev/null @@ -1,55 +0,0 @@ -package coil3 - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.ContentScale -import coil3.compose.AsyncImage -import coil3.disk.DiskCache -import coil3.memory.MemoryCache -import coil3.network.ktor3.KtorNetworkFetcherFactory -import coil3.request.CachePolicy -import coil3.request.crossfade -import coil3.util.DebugLogger -import okio.FileSystem - -fun PlatformContext.asyncImageLoader() = - ImageLoader - .Builder(this) - .components { add(KtorNetworkFetcherFactory()) } - .crossfade(true) - .networkCachePolicy(CachePolicy.ENABLED) - .diskCachePolicy(CachePolicy.ENABLED) - .memoryCachePolicy(CachePolicy.ENABLED) - .memoryCache { - MemoryCache.Builder() - .maxSizePercent(this, 0.25) - .strongReferencesEnabled(true) - .build() - } - .logger(DebugLogger()) - .build() - -/** - * Enable disk cache for the [ImageLoader]. - */ -fun ImageLoader.enableDiskCache() = this.newBuilder() - .diskCache { - DiskCache.Builder() - .directory(FileSystem.SYSTEM_TEMPORARY_DIRECTORY / "image_cache") - .build() - }.build() - - -@Composable -fun ImageLoader( - modifier: Modifier = Modifier, - url: String, - description: String -){ - AsyncImage( - model = url, - contentDescription = description, - contentScale = ContentScale.Fit, - modifier = modifier - ) -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/imageLoader/ImageLoader.kt b/composeApp/src/commonMain/kotlin/imageLoader/ImageLoader.kt new file mode 100644 index 0000000..ea3faa6 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/imageLoader/ImageLoader.kt @@ -0,0 +1,5 @@ +package imageLoader + +import com.seiko.imageloader.ImageLoader + +expect fun generateImageLoader(): ImageLoader \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/planets/PlanetDetailScreen.kt b/composeApp/src/commonMain/kotlin/planets/PlanetDetailScreen.kt index 53f9388..79a7b80 100644 --- a/composeApp/src/commonMain/kotlin/planets/PlanetDetailScreen.kt +++ b/composeApp/src/commonMain/kotlin/planets/PlanetDetailScreen.kt @@ -4,12 +4,11 @@ import androidx.compose.animation.AnimatedVisibilityScope import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.SharedTransitionScope import androidx.compose.animation.core.tween +import androidx.compose.foundation.Image import androidx.compose.foundation.ScrollState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxHeight @@ -45,13 +44,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.layout import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import coil3.ImageLoader -import coil3.compose.AsyncImage +import com.seiko.imageloader.rememberImagePainter import getPlatform import org.koin.compose.koinInject import planets.domain.domain.Planet @@ -173,7 +170,10 @@ private fun SharedTransitionScope.Mobile( .parallaxLayoutModifier(scrollState, 2) // .border(width = 1.dp, color = Color.White), ) { - ImageLoader( + val painter = rememberImagePainter(url = planet.img) + Image( + painter = painter, + contentDescription = planet.imgDescription, modifier = modifier .fillMaxSize() .sharedElement( @@ -182,13 +182,10 @@ private fun SharedTransitionScope.Mobile( boundsTransform = { _, _ -> tween(durationMillis = 1000) } - ), - url = planet.img, - description = planet.imgDescription + ) ) } - Column( modifier = modifier .padding(top = 32.dp) @@ -246,7 +243,10 @@ private fun SharedTransitionScope.NonMobile( .sizeIn(maxWidth = 500.dp, maxHeight = 500.dp) .clip(CircleShape) ) { - ImageLoader( + val painter = rememberImagePainter(url = planet.img) + Image( + painter = painter, + contentDescription = planet.imgDescription, modifier = modifier .fillMaxSize() .sharedElement( @@ -255,9 +255,7 @@ private fun SharedTransitionScope.NonMobile( boundsTransform = { _, _ -> tween(durationMillis = 1000) } - ), - url = planet.img, - description = planet.imgDescription + ) ) } diff --git a/composeApp/src/commonMain/kotlin/planets/PlanetsScreen.kt b/composeApp/src/commonMain/kotlin/planets/PlanetsScreen.kt index 3cd0cc7..776b50c 100644 --- a/composeApp/src/commonMain/kotlin/planets/PlanetsScreen.kt +++ b/composeApp/src/commonMain/kotlin/planets/PlanetsScreen.kt @@ -5,26 +5,19 @@ import androidx.compose.animation.AnimatedVisibilityScope import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.SharedTransitionScope import androidx.compose.animation.core.tween -import androidx.compose.foundation.background -import androidx.compose.foundation.border +import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight @@ -45,13 +38,10 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import coil3.ImageLoader +import com.seiko.imageloader.rememberImagePainter import getPlatform import kotlinx.coroutines.launch import org.koin.compose.koinInject @@ -147,7 +137,10 @@ fun SharedTransitionScope.PageItem( .fillMaxWidth() .heightIn(max = 400.dp) ) { - ImageLoader( + val painter = rememberImagePainter(url = planet.img) + Image( + painter = painter, + contentDescription = planet.imgDescription, modifier = modifier .fillMaxSize() .sharedElement( @@ -156,9 +149,7 @@ fun SharedTransitionScope.PageItem( boundsTransform = { _, _ -> tween(durationMillis = 1000) } - ), - url = planet.img, - description = planet.imgDescription, + ) ) } diff --git a/composeApp/src/desktopMain/kotlin/imageLoader/ImageLoader.desktop.kt b/composeApp/src/desktopMain/kotlin/imageLoader/ImageLoader.desktop.kt new file mode 100644 index 0000000..3b0ee95 --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/imageLoader/ImageLoader.desktop.kt @@ -0,0 +1,53 @@ +package imageLoader + +import com.seiko.imageloader.ImageLoader +import com.seiko.imageloader.component.setupDefaultComponents +import com.seiko.imageloader.intercept.painterMemoryCacheConfig +import okio.Path.Companion.toOkioPath +import java.io.File + +actual fun generateImageLoader(): ImageLoader { + return ImageLoader { + components { + setupDefaultComponents() + } + interceptor { + painterMemoryCacheConfig { + maxSize(50) + } + diskCacheConfig { + directory(getCacheDir().toOkioPath().resolve("image_cache")) + maxSizeBytes(512L * 1024 * 1024) // 512MB + } + } + } +} + +enum class OperatingSystem { + Windows, Linux, MacOS, Unknown +} + +private val currentOperatingSystem: OperatingSystem + get() { + val operSys = System.getProperty("os.name").lowercase() + return if (operSys.contains("win")) { + OperatingSystem.Windows + } else if (operSys.contains("nix") || operSys.contains("nux") || + operSys.contains("aix") + ) { + OperatingSystem.Linux + } else if (operSys.contains("mac")) { + OperatingSystem.MacOS + } else { + OperatingSystem.Unknown + } + } + +private fun getCacheDir() = when (currentOperatingSystem) { + OperatingSystem.Windows -> File(System.getenv("AppData"), "$ApplicationName/cache") + OperatingSystem.Linux -> File(System.getProperty("user.home"), ".cache/$ApplicationName") + OperatingSystem.MacOS -> File(System.getProperty("user.home"), "Library/Caches/$ApplicationName") + else -> throw IllegalStateException("Unsupported operating system") +} + +private const val ApplicationName = "Compose ImageLoader" \ No newline at end of file diff --git a/composeApp/src/iosMain/kotlin/imageLoader/ImageLoader.ios.kt b/composeApp/src/iosMain/kotlin/imageLoader/ImageLoader.ios.kt new file mode 100644 index 0000000..74edc2a --- /dev/null +++ b/composeApp/src/iosMain/kotlin/imageLoader/ImageLoader.ios.kt @@ -0,0 +1,34 @@ +package imageLoader + +import com.seiko.imageloader.ImageLoader +import com.seiko.imageloader.component.setupDefaultComponents +import com.seiko.imageloader.intercept.painterMemoryCacheConfig +import okio.Path.Companion.toPath +import platform.Foundation.NSCachesDirectory +import platform.Foundation.NSSearchPathForDirectoriesInDomains +import platform.Foundation.NSUserDomainMask + +actual fun generateImageLoader(): ImageLoader { + return ImageLoader { + components { + setupDefaultComponents() + } + interceptor { + painterMemoryCacheConfig { + maxSize(50) + } + diskCacheConfig { + directory(getCacheDir().toPath().resolve("image_cache")) + maxSizeBytes(512L * 1024 * 1024) // 512MB + } + } + } +} + +private fun getCacheDir(): String { + return NSSearchPathForDirectoriesInDomains( + NSCachesDirectory, + NSUserDomainMask, + true, + ).first() as String +} \ No newline at end of file diff --git a/composeApp/src/jsMain/kotlin/imageLoader/ImageLoader.js.kt b/composeApp/src/jsMain/kotlin/imageLoader/ImageLoader.js.kt new file mode 100644 index 0000000..31d5f7c --- /dev/null +++ b/composeApp/src/jsMain/kotlin/imageLoader/ImageLoader.js.kt @@ -0,0 +1,18 @@ +package imageLoader + +import com.seiko.imageloader.ImageLoader +import com.seiko.imageloader.component.setupDefaultComponents +import com.seiko.imageloader.intercept.bitmapMemoryCacheConfig + +actual fun generateImageLoader(): ImageLoader { + return ImageLoader { + components { + setupDefaultComponents() + } + interceptor { + bitmapMemoryCacheConfig { + maxSize(32 * 1024 * 1024) // 32MB + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/jsMain/kotlin/main.kt b/composeApp/src/jsMain/kotlin/main.kt index e746d54..332fad2 100644 --- a/composeApp/src/jsMain/kotlin/main.kt +++ b/composeApp/src/jsMain/kotlin/main.kt @@ -1,12 +1,14 @@ import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.window.CanvasBasedWindow +import androidx.compose.ui.window.ComposeViewport +import kotlinx.browser.document import org.jetbrains.skiko.wasm.onWasmReady @OptIn(ExperimentalComposeUiApi::class) fun main() { onWasmReady { - CanvasBasedWindow("SolarSystemKMP") { - App(disableDiskCache = true) + val body = document.body ?: return@onWasmReady + ComposeViewport(body) { + App() } } } diff --git a/composeApp/src/jsMain/resources/index.html b/composeApp/src/jsMain/resources/index.html index 2ef01a4..eb0a12f 100644 --- a/composeApp/src/jsMain/resources/index.html +++ b/composeApp/src/jsMain/resources/index.html @@ -1,12 +1,21 @@ - +
+